Аутентификация

Дата обновления перевода 2023-07-20

Аутентификация

Когда запрос указывает на безопасную область, а один из слушателей из карты брандмауэра может извлечь личные данные пользователя из текущего объекта Request, он должен создать токен, содержащий менеджера аутентификации, чтобы валидировать данный токен и вернуть аутентифицированный токен, если предоставленные данные были определены валидными. Слушатель потом должен сохранить аутентифицированный токен, используя хранилище токенов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class SomeAuthenticationListener
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var AuthenticationManagerInterface
     */
    private $authenticationManager;

    /**
     * @var string Uniquely identifies the secured area
     */
    private $providerKey;

    // ...

    public function __invoke(RequestEvent $event)
    {
        $request = $event->getRequest();

        $username = ...;
        $password = ...;

        $unauthenticatedToken = new UsernamePasswordToken(
            $username,
            $password,
            $this->providerKey
        );

        $authenticatedToken = $this
            ->authenticationManager
            ->authenticate($unauthenticatedToken);

        $this->tokenStorage->setToken($authenticatedToken);
    }
}

Note

Токен может быть любым классом, если он реализует TokenInterface.

Менеджер аутентификации

Менеджер аутентификации по умолчанию является экземпляром AuthenticationProviderManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

// экземпляры Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface
$providers = array(...);

$authenticationManager = new AuthenticationProviderManager($providers);

try {
    $authenticatedToken = $authenticationManager
        ->authenticate($unauthenticatedToken);
} catch (AuthenticationException $exception) {
    // аутентификация неудачна
}

При инстанциировании, AuthenticationProviderManager получает несколько поставщиков аутентификации, каждый из которых поддерживает разные виды токенов.

Note

Вы можете конечно же написать собственный менеджер аутентификации, только он должен реализовывать AuthenticationManagerInterface.

Поставщики аутентификации

Каждый поставщик (так как он реализует AuthenticationProviderInterface) имеет метод supports(), по которому AuthenticationProviderManager может определить, поддерживается ли данный токен. Если это так, то менеджер вызывает метод поставщика authenticate(). Этот метод должен вернуть аутнетифицированный токен или вызвать AuthenticationException (или любое другео исключение, расширяющее его).

Аутентификация пользователей по имени и паролю

Поставщик аутентификации будет пытаться аутентифицировать пользователя, основываясь на предоставленных личных данных. Обычно это имя пользователя и пароль. Большинство веб-приложений хранят свои имена пользоваталей и хеш пользовательского пароля, в сочетании с рандомно сгенерированной солью. Это означает, что среднестатистическая аутентификация будет состоять из извлечения соли и хешированного пароля из хранилища данных пользователей, хеширования пароля, только что предоставленного пользователем (например, используя форму входа) с солью и их сравнения для определения валидности заданного пароля.

Этот функционал предоставлен DaoAuthenticationProvider. Он извлекает данные пользователя изUserProviderInterface, использует UserPasswordHasherInterface для создания хеша пароля и возвращает аутентифицированный токен, если пароль валиден:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\User\UserChecker;

// Класс 'InMemoryUser' был представлен в Symfony 5.3.
// В предыдущих версиях он назывался 'User'
$userProvider = new InMemoryUserProvider(
    [
        'admin' => [
            // пароль - "foo"
            'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
            'roles'    => ['ROLE_ADMIN'],
        ],
    ]
);

// для дополнительных проверок: подключен/закрыт/истёк ли срок действия аккаунта
$userChecker = new UserChecker();

// массив кодировщиков паролей (см. ниже)
$hasherFactory = new PasswordHasherFactoryInterface(...);

$daoProvider = new DaoAuthenticationProvider(
    $userProvider,
    $userChecker,
    'secured_area',
    $hasherFactory
);

$daoProvider->authenticate($unauthenticatedToken);

Note

Пример выше демонстрирует использование поставщика пользователей "оперативной памяти", но вы можете использовать любого другого поставщика пользователей, если он реализует UserProviderInterface Также возможно позволить нескольким поставщикам пользователей найти данные пользователя, используя :class:Symfony\Component\Security\Core\User\ChainUserProvider`.

Фабрика кодировщиков паролей

DaoAuthenticationProvider использует фабрику кодировщиков для создания кодировщика паролей для заданного типа пользователя. Это позволяет вам использовать разные стратегии шифрования для разных типов пользователей. По умолчанию, PasswordHasherFactory получает массив кодировщиков:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Acme\Entity\LegacyUser;
use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\Security\Core\User\InMemoryUser;

$defaultHasher = new MessageDigestPasswordHasher('sha512', true, 5000);
$weakHasher = new MessageDigestPasswordHasher('md5', true, 1);

$hashers = [
    InMemoryUser::class => $defaultHasher,
    LegacyUser::class   => $weakHasher,
    // ...
];
$hasherFactory = new PasswordHasherFactory($hashers);

Каждый кодировщик должен реализовывать UserPasswordHasherInterface или быть массивом с class и ключом arguments, который позволяется фабрике кодировщиков конструировать кодировщик только по необходимости.

Создание пользовательского кодировщика паролей

Существует множество встроенных кодировщиков паролей. Но если вам нужно создать собственный, он должен следовать таким правилам:

  1. Класс должен реализовывать UserPasswordHasherInterface (вы также можете расширить UserPasswordHasher);
  2. Реализация hashPassword() и isPasswordValid() должна вначале убедиться, что пароль не слишком длинный, т.е. длина пароля не превышает 4096 символов. Это из соображений безопасности (см. CVE-2013-5750), и вы можете использовать метод isPasswordTooLong() для такой проверки:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
    use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
    use Symfony\Component\Security\Core\Exception\BadCredentialsException;
    
    class FoobarHasher extends UserPasswordHasher
    {
        use CheckPasswordLengthTrait;
    
        public function hashPassword(UserInterface $user, string $plainPassword): string
        {
            if ($this->isPasswordTooLong($user->getPassword())) {
                throw new BadCredentialsException('Invalid password.');
            }
    
            // ...
        }
    
        public function isPasswordValid(UserInterface $user, string $plainPassword)
        {
            if ($this->isPasswordTooLong($user->getPassword())) {
                return false;
            }
    
            // ...
        }
    }

Использование кодировщиков паролей

Когда метод getPasswordHasher() фабрики кодировщиков паролей вызывается с объектом пользователя в качестве первого аргумента, он будет возвращать кодировщик типа PasswordHasherInterface, который должен быть использован для шифрованя пароля пользователя:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// экземпляр Acme\Entity\LegacyUser
$user = ...;

// отправленный пароль, например, при регистрации
$plainPassword = ...;

$hasher = $hasherFactory->getPasswordHasher($user);

// возвращает $weakHasher (см. выше)
$hashedPassword = $hasher->hashPassword($user, $plainPassword);

$user->setPassword($hashedPassword);

// ... сохраните пользователя

Теперь, когда вы хотите проверить, является ли отправленный пароль (например, при попытке входа в систему) правильным, вы можете использовать:

1
2
3
4
5
6
7
// извлечь Acme\Entity\LegacyUser
$user = ...;

// отпавленный пароль, например, из формы входа
$plainPassword = ...;

$validPassword = $hasher->isPasswordValid($user, $plainPassword);

События аутентификации

Компонент безопасности предоставляет 4 связанных с аутентификацией события:

???????? ????????? ??????? ????????, ?????????? ?????????
security.authentication.success AuthenticationEvents::AUTHENTICATION_SUCCESS AuthenticationEvent
security.authentication.failure AuthenticationEvents::AUTHENTICATION_FAILURE AuthenticationFailureEvent
security.interactive_login SecurityEvents::INTERACTIVE_LOGIN InteractiveLoginEvent
security.switch_user SecurityEvents::SWITCH_USER SwitchUserEvent
security.logout_on_change Symfony\Component\Security\Http\Event\DeauthenticatedEvent::class DeauthenticatedEvent

События удачной и неудачной аутентификации

Когда поставщик аутентифицирует пользователя, запускается событие security.authentication.success. Но будьте осторожны - это событие будет запускаться, к примеру, при каждом запросе, если у вас аутентификация основывается на сессии. См. security.interactive_login ниже, если вам нужно сделать что-то, когда пользователь действительно выполняет вход.

5.4

Опция always_authenticate_before_granting устарела в Symfony 5.4, и будет удалена в 6.0.

Когда поставщик пробует выполнть аутентификацию, но терпит неудачу (т.е. вызывает AuthenticationException), запускается событие security.authentication.failure. Вы можете слушать например событие security.authentication.failure для того, чтобы вести логи неудачных попыток входа.

События безопасности

Событие security.interactive_login вызывается после того, как пользователь выполнил активный вход на ваш сайт. Важно отличать это действие от неинтерактивных методов аутентификации вроде:

  • аутентификация, основанная на вашей сессии
  • аутентификация, используя базовый HTTP заголовок.

Вы можете слушать событие security.interactive_login, например, чтобы отображать вашему пользователю приветственное флеш-собщение каждый раз, когда он выполняет вход.

Событие security.switch_user запускается каждый раз, когда вы активируете слушателя брандмауэра switch_user.

Событие Symfony\Component\Security\Http\Event\DeauthenticatedEvent вызывается, когда токен был деаутентифицирован из-за изменений пользователя, оно может помочь вам с некоторыми задачи по уборке.

See also

Чтобы узнать больше о переключении пользователей, см. Как имперсонализировать пользователя.