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

Дата обновления перевода 2025-09-15

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

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

Чтобы сэкономить время, вы можете установить Symfony Maker и позволить Symfony сгенерировать новый аутентификатор, выполнив следующую команду:

1
2
3
4
5
6
7
8
9
$ php bin/console make:security:custom

  Какое имя класса аутентификатора (например, CustomAuthenticator):
  > ApiKeyAuthenticator

  updated: config/packages/security.yaml
  created: src/Security/ApiKeyAuthenticator.php

  Успех!

Откройте файл src/Security/ApiKeyAuthenticator.php, созданный этой командой, и вы найдете что-то вроде этого:

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);
    }
}

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

Tip

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

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

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($userIdentifier), $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
        );
    }
}

Некоторые приложения нормализуют идентификаторы пользователей перед их обработкой. Например, перевод идентификаторов в нижний регистр помогает рассматривать такие значения, как "john.doe",
"John.Doe" или "JOHN.DOE" как эквивалентные в системах, где идентификаторы не чувствительны к регистру.

При необходимости вы можете передать нормализатор как третий аргумент UserBadge. Это вызываемое получает $userIdentifier и должно вернуть строку.

7.3

Поддержка нормализаторов идентификаторов пользователей была представлена в Symfony 7.3.

Пример ниже использует нормализатор, который преобразует имена пользователей в нормализованный, исключительно ASCII, формат в нижнем регистре:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Security/NormalizedUserBadge.php
namespace App\Security;

use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use function Symfony\Component\String\u;

final class NormalizedUserBadge extends UserBadge
{
    public function __construct(string $identifier)
    {
        $callback = static fn (string $identifier): string => u($identifier)->normalize(UnicodeString::NFKC)->ascii()->lower()->toString();

        parent::__construct($identifier, null, $callback);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Security/PasswordAuthenticator.php
namespace App\Security;

final class PasswordAuthenticator extends AbstractLoginFormAuthenticator
{
    // упрощено для краткости
    public function authenticate(Request $request): Passport
    {
        $username = (string) $request->request->get('username', '');
        $password = (string) $request->request->get('password', '');

        $request->getSession()
            ->set(SecurityRequestAttributes::LAST_USERNAME, $username);

        return new Passport(
            new NormalizedUserBadge($username),
            new PasswordCredentials($password),
            [
                // всі інші корисні бейджі
            ]
        );
    }
}

Полномочия пользователя

Полномочия пользователя используются для аутентификации пользователя, то есть для проверки валидности предоставленной информации (такой как пароль, токен API или пользовательские полномочия).

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

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'));
    }
}