Использование новой безопасности, основанной на аутентификаторе

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

Использование новой безопасности, основанной на аутентификаторе

5.1

Безопасность, основанная на аутентификаторе, была представлена в Symfony 5.1.

В Symfony 5.1 была представлена новая система аутентификации. Эта система изменяет внутренности Безопасности Symfony, чтобы сделать ее более расширяемой и более понимаемой.

Подключение системы

Система, основанная на аутентификации, может быть подключена с использованием настройки enable_authenticator_manager:

1
2
3
4
# config/packages/security.yaml
security:
    enable_authenticator_manager: true
    # ...

Новая система обратно совместима с текущей системой аутентификации, с некоторыми исключениями, которые будут разъясняться в этой статье:

Добавление поддержки для небезопасного доступа (т.е. анонимные пользователи)

В Symfony, пользователи, которые еще не выполнили вход на ваш сайт, назывались анонимными пользователями . Новая система больше не имеет анонимной аутентификации. Вместо этого, такие сессии теперь рассматриваются, как неаутентифицированные (т.е. без токена безопасности). При использовании isGranted(), результат всегда будет false (т.е. отказ), так как эта сессия рассматривается, как пользователь без привелегий.

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

1
2
3
4
5
6
7
8
9
10
11
# config/packages/security.yaml
security:
    enable_authenticator_manager: true

    # ...
    access_control:
        # разрешить неаутентифицированным пользователям доступ к форме входа
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }

        # но потребовать аутентификации для всех других маршрутов админа
        - { path: ^/admin, roles: ROLE_ADMIN }

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

5.2

Класс NullToken был представлен в Symfony 5.2.

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

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

// ...
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // ...

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
    {
        // ...

        if ($token instanceof NullToken) {
            // пользователь не аутентифицирован, например, позволить ему просмотр
            // только публичных постов
            return $subject->isPublic();
        }
    }
}

Конфигурация точки входа аутентификации

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

Вы можете сконфигурировать это, используя настройку entry_point:

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/packages/security.yaml
security:
    enable_authenticator_manager: true

    # ...
    firewalls:
        main:
            # позволить аутентификацию используя форму или базовый HTTP
            form_login: ~
            http_basic: ~

            # сконфигурировать форму аутентификации в качестве точки входа для неаутентифицированных пользователей
            entry_point: form_login

Note

Вы также можете создать собственную точку входа аутентификации, создав класс, который реализует AuthenticationEntryPointInterface. Вы затем можете установить entry_point в id сервиса (например, entry_point: App\Security\CustomEntryPoint)

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

Безопасность традиционно можно расширить, написав пользовательских поставщиков аутентификации. Система, основанная на аутентификаторе, отменила поддержку этих провайдеров, и представила новый интерфейс аутентификатора в качестве основы для методов пользовательской аутентификации.

Tip

Аутентификаторы Guard все еще поддерживаются в системе, основанной на аутентификаторах. Однако, рекомендуется также обновить их, если вы реорганизуете свое приложение в новую систему. Новый интерфейс аутентификтора имеет много сходств с интерфейсом аутентификтора guard, что делает переписывание кода легче.

Аутентификаторы должны реализовывать 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\PassportInterface;
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): PassportInterface
    {
        $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);
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/packages/security.yaml
security:
    enable_authenticator_manager: true

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

            # не забудьте также сконфигурировать entry_point, если
            # аутентификатор реализует AuthenticationEntryPointInterface
            # entry_point: App\Security\CustomFormLoginAuthenticator

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

Tip

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

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

5.2

UserBadge был представлен в Symfony 5.2. До версии 5.2, экземпляр пользователя предоставлялся напрямую паспорту.

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

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

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

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

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

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

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

class CustomAuthenticator extends AbstractAuthenticator
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

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

        return new Passport(
            new UserBadge($email, function ($userIdentifier) {
                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
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;

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

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

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

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

Знаки паспорта

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

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

5.2

С версии 5.2, 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
27
// 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;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;

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

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

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

Tip

Кроме знаков, паспорта могут определять атрибуты, которые позволяют методу authenticate() хранить произвольную информацию в паспорте, чтобы получить доступ к ней из других методов аутентификатора (например, 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
// ...
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class LoginAuthenticator extends AbstractAuthenticator
{
    // ...

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

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

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

        return $passport;
    }

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

5.2

Атрибуты паспортов были представлены в Symfony 5.2.