Как создать пользовательский аутентификатор
Дата обновления перевода 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'));
}
}