Казалось бы, что может быть проще авторизации, но у Symfony2 на это свое мнение и я хочу поделиться с Вами, как это сделать правильно.
Вариант авторизации, который я предлагаю, работает начиная с версии 2.4 и могу Вас заверить, это самый быстрый и простой для понимания, из всех остальных предлагаемых правильных вариантов авторизации.
1. попробуйте сначала выполнить все указанные мной действия на только-что установленной Symfony, потом уже будете пробовать на своем уже работающем проекте, иначе можете запутаться.
2. изначально, в Symfony нахождение пользователя в базе данных происходит по полю username, которое в Вашей базе данных должно быть уникальным (Вы это увидите ниже), а для нахождения пользователя Symfony использует класс, который называет провайдер, я этот класс назвал UserProvider, об этом позже.
И так, поехали, настроим app/config/security.yml
security:
encoders:
Acme\DemoBundle\Entity\User:
algorithm: sha1
encode_as_base64: false
iterations: 1
providers:
webservice:
id: userProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
demo_secured_area:
anonymous: true
pattern: ^/
simple_form:
authenticator: userAuthenticator
check_path: _demo_security_check
login_path: _demo_login
logout:
path: _demo_logout
target: _demo
access_control:
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }
Объявим 2 сервиса в файле app/config/services.yml или app/config/config.yml
services:
userProvider:
class: Acme\DemoBundle\Security\UserProvider
userAuthenticator:
class: Acme\DemoBundle\Security\UserAuthenticator
arguments: ["@security.password_encoder"]
Теперь создадим эти 2 сервиса:
Сначала создадим класс проверки пароля src/Acme/DemoBundle/Security/UserAuthenticator.php
<?php
/**
* По мотивам http://symfony.com/doc/current/cookbook/security/custom_password_authenticator.html
* Класс авторизации (не аутентификации) - возвращает менеджеру ( Symfony AuthenticationManager ), а тот, в свою
* очередь listener’y авторизованный token или вызывает Exception с ошибкой. Для проверки пользователя использует
* userProvider, который и проверяет существует ли пользователь.
*/
namespace Acme\DemoBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserAuthenticator implements SimpleFormAuthenticatorInterface
{
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Invalid username or password');
}
$passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());
if ($passwordValid) {
return new UsernamePasswordToken(
$user,
$user->getPassword(),
$providerKey,
$user->getRoles()
);
}
throw new AuthenticationException('Invalid password');
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken && $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
}
выше описанный класс, проверяет правильность пароля с помощью метода isPasswordValid() и закономерный вопрос - как он может проверить правильность пароля, ведь он не знает данные пользователя? Вы правы, и для этого он пользуется провайдером - классом, который умеет вытягивать данные из базы данных. Так давайте создадим этот класс - провайдер src/Acme/DemoBundle/Security/UserProvider.php, который будет находить пользователя и его пароль в базе данных, по полю username
<?php
/**
* По мотивам: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html
* Провайдер пользователя - находит пользователя по уникальному полю username.
*/
namespace Acme\DemoBundle\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Acme\DemoBundle\Entity\User;
class UserProvider implements UserProviderInterface
{
/**
* находит и возвращает экземпляр класса User или выбрасывает Exception.
*
* @param string $username - мыло пользователя
*
* @return false|User|\Symfony\Component\Security\Core\User\UserInterface
* @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function loadUserByUsername( $username = '' )
{
if (empty($username)) {
throw new UsernameNotFoundException('Username is empty.');
}
$r = mysq_fetch_assoc(mysql_query("SELECT * FROM User WHERE username = '".$username."'"));
if(empty($r)){
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
$user = new User();
$user->setUsername($r['username']);
$user->setPassword($r['password']);// напомню, это хэш пароля (как его создать - читай ниже)
return $user;
}
/**
* метод проверяет вид сущности пользователя (ведь их может быть много)
*
* @param UserInterface $user
*
* @return User|UserInterface
* @throws \Symfony\Component\Security\Core\Exception\UnsupportedUserException
*/
public function refreshUser( UserInterface $user )
{
if (!$user instanceof User) {
throw new UnsupportedUserException( sprintf( 'Instances of "%s" are not supported.', get_class( $user ) ) );
}
return $this->loadUserByUsername( $user->getUsername() );
}
/**
* Метод проверки класса пользователя
* нужен чтобы Symfony использовал правильный класс Пользователя для получения объекта пользователя
*
* @param string $class
*
* @return bool
*/
public function supportsClass( $class )
{
return $class === 'Acme\\DemoBundle\\Entity\\User';
}
}
Имейте ввиду - пароль в базе данных должен храниться в виде хэша, ведь выше, в файле мы описали алгоритм хэширования пароля. Как создавать хэш пароля, напишу в самом низу статьи.
И наконец мы обязаны создать класс, описывающий поля пользователя src/Acme/DemoBundle/Entity/User.php
<?php
namespace Acme\DemoBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
class User implements UserInterface, EquatableInterface
{
private $id;
private $username;
private $password;
private $salt;
public function setId( $v )
{
$this->id = $v;
}
public function setUsername( $v )
{
$this->username = $v;
}
public function setPassword( $v )
{
$this->password = $v;
}
/**
* @inheritDoc
* создан, т.к. этого требует интерфейс UserInterface
* @return string - обязан возвращать уникальный параметр пользователя (например уникальное имя или емэйл)
*/
public function getUsername()
{
return (string)$this->username;
}
/**
* @inheritDoc
* создан, т.к. этого требует интерфейс UserInterface
* отдает соль для хэширования пароля пользователя
* @return null|string
*/
public function getSalt()
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return null;
}
/**
* @inheritDoc
*/
public function getPassword()
{
return $this->password;
}
/**
* @inheritDoc
*/
public function getRoles()
{
return empty($this->roles)? array('ROLE_USER') : explode(',', $this->roles);
}
/**
* @inheritDoc
* создан, т.к. этого требует интерфейс UserInterface
* идея в следующем: удаляет конфиденциальные данные о пользователе. Это важно, т.к.,
* конфиденциальная информация (например, plain-text password хранится в этом объекте).
*/
public function eraseCredentials()
{
}
/**
* Данный метод вызывается, если сущность имплементирует Symfony\Component\Security\Core\User\EquatableInterface
*
* Проверка объекта пользователя полученного из токена с объектом полученным из провайдера (но только если в
* app/config/security.yml выставлен access_control).
*
* Описание процесса:
* При каждом HTTP-request-e происходит следующее:
* 1) объект пользователя поднимается из сессии с помощью десериализации, см. функцию:
* \Symfony\Component\Security\Http\Firewall\ContextListener::safelyUnserialize() и сохраняется в объект токена.
* 2) зная ID пользователя, Symfony идет в UserProvider который обязан возвращить пользователя по ID пользователя -
* \Symfony\Bridge\Doctrine\Security\User\EntityUserProvider::refreshUser() - данная функция ходит за пользователем
* в UserProvider и умеет это делать по-разному:
* - если объект пользователя не реализует \Symfony\Component\Security\Core\User\UserInterface то объект валиден
* - если UserRepository имплементирует интерфейс \Symfony\Component\Security\Core\User\UserProviderInterface
* - или наш случай: $repository->find($id)
*
* Следовательно, когда вызывается данный метод, то мы находимся в объекте, который создан из сессии, а объект $user
* который передан в функцию является объектом полученным из UserProvider-a, и если мы подтверждаем, что объекты
* валидны, то объект $user (который передан в функцию) будет использоваться в качестве авторизованного
* пользователя. Получается, что в данной функции мы должны проверить значения полей, которые мы считаем критичными
* для авторизации (например логин и пароль, заодно можно провреить ID-пользователя). Хорошим примером проверок
* является функция \Symfony\Component\Security\Core\User\User::isEqualTo()
*
* @inheritDoc
*/
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof self) {
return false;
}
if ($this->getUsername() !== $user->getUsername()) {
return false;
}
if ($this->getPassword() !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
return true;
}
}
Собственно это все, теперь самое время почистить кэш: rm -rf app/cache/* и попробовать авторизоваться на странице http://symfony.local/app_dev.php/demo/secured/login
Как создавать хэш пароля
В каком-нибудь из своих контроллеров воспользуйтесь ниже описанным кодом. Итак, первый вариант - простой:
$user = new User();
$hash = $this->get('security.password_encoder')->encodePassword($user, 'пароль пользователя');
Второй вариант - сложнее (для заметки):
$user = new User();
$hash = $this->get('security.encoder_factory')->getEncoder($user)->encodePassword('пароль пользователя', $user->getSalt());
Вот такие пироги товарищи, надеюсь разжевал хорошо, но если будут вопросы - подумайте и затем пишите, удачки всем.
Комментарии
Комментариев пока нет.