Як створити користувацький аутентифікатор

Дата оновлення перекладу 2024-06-03

Як створити користувацький аутентифікатор

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 "Неавторизовано"
            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), []);

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

        return $passport;
    }

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