Как создать пользовательский аутентификатор

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

Как создать пользовательский аутентификатор

Symfony поставляется со множеством аутентификаторов и сторонние пакеты также реализуют более сложные случаи, вроде JWT и oAuth 2.0. Однако, иногда вам нужно реализовать пользовательский механизм аутентификации, который еще не существует, или вам нужно настроить уже существующим. В таких случаях, вы должны создать и использовать собственый аутентификатор.

Аутентификаторы должны реализовывать AuthenticatorInterface. Вы также можете расширить AbstractAuthenticator, который имеет реализацию по умолчанию для метода createAuthenticatedToken(), который подходят для большинства случаев применения:

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
45
46
47
48
49
50
51
52
53
54
55
56
// src/Security/ApiKeyAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiKeyAuthenticator extends AbstractAuthenticator
{
    /**
     * Вызывается по укаждому запросу, чтобы решить, должен ли быть использован этот
     * аутентификатор для запроса. Возвращение `false` приведет к пропуску этого аутентификатора.
     */
    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-AUTH-TOKEN');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-AUTH-TOKEN');
        if (null === $apiToken) {
            // Заголовок токена был пустым, аутентификация будет неуспешна, с HTTP
            // статус-кодом 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }

        return new SelfValidatingPassport(new UserBadge($apiToken));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // при успехе, позволить запросу продолжить
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $data = [
            // вы можете захотеть настроить или скрыть сообщение для начала
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // или перевести это сообщение
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }
}

Tip

Если ваш пользовательский аутентификатор является формой входа, вы можете также расширить из класса AbstractLoginFormAuthenticator, чтобы облегчить себе работу.

Аутентификатор можно подключить, используя настройку custom_authenticators:

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:

    # ...
    firewalls:
        main:
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator

Tip

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

Метод authenticate() - это самый важный метод аутентификатора. Его работа заключается в извлечении информации идентификационных данных (к примеру, имени пользователя и пароля, или API-токенов) из объекта Request, и их преобразовании в безопасный Passport. См. ниже, чтобы детально узнать о процессе аутентифкации.

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

onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response

Если пользователь аутентифицирован, этот метод вызывается с аутентифицированным $token. Этот метод может вернуть ответ (например, перенаправить пользователя на домашнюю страницу).

Если возвращается null, запрос продолжается, как обычно (т.е. вызывается контроллер, совпадающий с маршрутом входа в систему). Это полезно для маршрутов API, где каждый маршрут защищен заголовком ключа API.

onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response

Если во время аутентификации вызывается AuthenticationException, процесс терпит неудачу, и вызывается этот метод. Этот метод может вернуть ответ (например, вернуть ответ 401 Неавторизовано в маршрутах API).

Если возвращается null, запрос продолжается, как обычно. Это полезно, к примеру, для форм входа в систему, где контроллер входа в систему запускается снова, с ошибками входа в систему.

Если вы используете дросселирование входа в систему , вы можете проверить, является ли $exception экземпляром TooManyLoginAttemptsAuthenticationException (например, чтобы отобразить соответствующее сообщение).

Внимание: Никогда не используйте $exception->getMessage() для экземпляров AuthenticationException. Это сообщение может содержать чувствительную информацию, которую вы не хотите демонстрировать публично. Вместо этого, используйте $exception->getMessageKey() и $exception->getMessageData(), как показано в полном примере выше. Используйте CustomUserMessageAuthenticationException, если вы хотите устанавливать пользовательские сообщения об ошибках.

Tip

Если ваш метод входа в систему интерактивный, что означает, что пользователь активно выполняет вход в ваше приложение, вы можете захотеть, чтобы ваш аутентификтор реализовывал InteractiveAuthenticatorInterface, чтобы он развертывал InteractiveLoginEvent

Паспорта безопасности

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

По умолчанию, Passport требует пользователя и идентификационных данных.

Используйте UserBadge, чтобы присоединить пользователя к паспорту. UserBadge требует идентификатор пользователя (например, имя пользователя или адрес электронной почты), который используется для загрузки пользователя при использовании поставщика пользователей :

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

// ...
$passport = new Passport(new UserBadge($email), $credentials);

Note

Максимальная разрешенная длина для идентификатора пользователя составляет 4096 знаков, чтобы предупредить атаки затоплений хранилища сессий.

Note

Вы можете по желанию передать загрузчик пользователей в качестве второго аргумента UserBadge. Это вызываемое получает $userIdentifier, и должно вернуть объект UserInterface (иначе вызывается UserNotFoundException):

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
// src/Security/CustomAuthenticator.php
namespace App\Security;

use App\Repository\UserRepository;
// ...

class CustomAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private UserRepository $userRepository,
    ) {
    }

    public function authenticate(Request $request): Passport
    {
        // ...

        return new Passport(
            new UserBadge($email, function (string $userIdentifier): ?UserInterface {
                return $this->userRepository->findOneBy(['email' => $userIdentifier]);
            }),
            $credentials
        );
    }
}

Следующие классы идентификационных данных поддерживаются по умолчанию:

PasswordCredentials

Это требует простого текстового $password, который валидируется с использованием кодировщика паролей, сконфигурированного для пользователя :

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

// ...
return new Passport(new UserBadge($email), new PasswordCredentials($plaintextPassword));
CustomCredentials

Позволяет пользовательскому замыканию проверять идентификационные данные:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;

// ...
return new Passport(new UserBadge($email), new CustomCredentials(
    // Если эта функция возвращает что-либо, кроме `true`, идентификационные данные
    // помечаются, как не валидные.
    // Параметр $credentials равняется следующему аргументу этого класса
    function (string $credentials, UserInterface $user): bool {
        return $user->getApiToken() === $credentials;
    },

    // Пользовательские идентификационные данные
    $apiToken
));

Паспорт самовалидации

Если вам не нужно проверять идентификационные данные (например, при использовании токенов API), вы можете использовать SelfValidatingPassport. Этот класс требует только объект UserBadge, и, по желанию, Знаки паспорта.

Бейджи паспорта

Passport также дополнительно позволяет вам добавлять бейджи безопасности. Бейджи добавляют паспорту больше данных (чтобы расширить безопасность). По умолчанию, следующие бейджи поддерживаются:

RememberMeBadge
Когда этот бейдж добавляется к паспорту, аутентификатор обозначает, что поддерживается "запомнить меня". Используется ли на самом деле "запомнить меня", зависит от специальной конфигурации remember_me. Прочтите Как добавить функциональность входа в систему "Запомнить меня", чтобы узнать больше.
PasswordUpgradeBadge
Это используется для автоматического обновления пароля до нового хеша после успешного входа в систему. Этот знак требует простого текстового пароля и установщика обновлений паролей (например, хранилище пользователей). См. Как мигрировать хеш пароля.
CsrfTokenBadge
Автоматически валидирует CSRF-токены для этого аутентификатора во время аутентификации. Конструктор требует ID токена (уникальны для каждой формы) и CSRF-токен (уникальный для каждого запроса). См. Как реализовать CSRF-защиту.
PreAuthenticatedUserBadge
Означает, что этот пользователь был предварительно аутентифицирован (т.е. до запуска Symfony). Пропускает пред-аутентификационную проверку пользователей.

Note

PasswordUpgradeBadge автоматически добавляется к паспорту, если паспорт имеет PasswordCredentials.

Например, если вы хотите добавить CSRF к вашему пользовательскому аутентификатору, вы запустите паспорт таким образом:

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
// src/Service/LoginAuthenticator.php
namespace App\Service;

// ...
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;

class LoginAuthenticator extends AbstractAuthenticator
{
    public function authenticate(Request $request): Passport
    {
        $password = $request->getPayload()->get('password');
        $username = $request->getPayload()->get('username');
        $csrfToken = $request->getPayload()->get('csrf_token');

        // ... валидировать, что ни один параметр не пуст

        return new Passport(
            new UserBadge($username),
            new PasswordCredentials($password),
            [new CsrfTokenBadge('login', $csrfToken)]
        );
    }
}

Атрибуты паспорта

Кроме бейджей, паспорта могут определять атрибуты, что позволяет методу authenticate() хранить произвольную информацию в паспорте, чтобы получать к нему доступ из других методов аутентификатора (например, createToken()):

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
// ...
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class LoginAuthenticator extends AbstractAuthenticator
{
    // ...

    public function authenticate(Request $request): Passport
    {
        // ... обработать запрос

        $passport = new SelfValidatingPassport(new UserBadge($username), []);

        // установить пользовательский атрибут (например, область действия)
        $passport->setAttribute('scope', $oauthScope);

        return $passport;
    }

    public function createToken(Passport $passport, string $firewallName): TokenInterface
    {
        // прочитать значение атрибута
        return new CustomOauthToken($passport->getUser(), $passport->getAttribute('scope'));
    }
}