Блог Андрея

 
 

Перевод раздела документации Symfony 5.0 Custom Authentication System with Guard (API Token Example)

Система пользовательской аутентификации с защитой (пример API-токена)

Оригинал статьи

Guard Authenticator может использоваться для:

и многое другое. В этом примере мы создадим систему аутентификации токена API, чтобы подробнее узнать о Guard.

Шаг 1) Подготовьте свой класс пользователя

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

Во-первых, убедитесь, что вы следовали главному Руководству по безопасности, когда создавали свой класс User. Затем, для простоты, добавьте свойство apiToken непосредственно в ваш класс User (хороший способ сделать это - команда make: entity):


// src/Entity/User.php
// ...

class User implements UserInterface
{
    // ...

     /**
      * @ORM\Column(type="string", unique=true, nullable=true)
      */
     private $apiToken;

    // the getter and setter methods
}

Не забудьте сгенерировать и выполнить миграцию:

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Шаг 2) Создайте класс аутентификатора

Чтобы создать собственную систему аутентификации, создайте класс реализуюзщий AuthenticatorInterface. Или расширите более простой AbstractGuardAuthenticator.

Это требует от вас реализации нескольких методов:

// src/Security/TokenAuthenticator.php
namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
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\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    private $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

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

    /**
     * Вызывается при каждом запросе. Верните все учетные данные, 
     * которые вы хотите передать getUser(), как $credentials.
     */
    public function getCredentials(Request $request)
    {
        return $request->headers->get('X-AUTH-TOKEN');
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        if (null === $credentials) {
            // Заголовок токена был пуст, проверка подлинности не выполняется
            // с кодом состояния HTTP 401 «Не авторизован»
            return null;
        }

        // если возвращается пользователь, вызывается checkCredentials ()
        return $this->em->getRepository(User::class)
            ->findOneBy(['apiToken' => $credentials])
        ;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // Проверьте учетные данные - например, убедитесь, что пароль действителен.
        // В случае использования токена API проверка учетных данных не требуется.

        // Верните `true`, чтобы вызвать успешную аутентификацию
        return true;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // в случае успеха, пусть запрос продолжается
        return null;
    }

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

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

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

    /**
     * Вызывается, когда требуется аутентификация, но она не отправляется
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $data = [
            // вы могли бы перевести это сообщение
            'message' => 'Authentication Required'
        ];

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

    public function supportsRememberMe()
    {
        return false;
    }
}

Хорошая работа! Каждый метод объясняется ниже: Методы Guard Authenticator.

Шаг 3) Настройте Аутентификатор

Для этого убедитесь, что ваш аутентификатор зарегистрирован как сервис. Если вы используете конфигурацию services.yaml по умолчанию, это происходит автоматически.

В заключении, настройте ключ брандмауэра в security.yaml для использования этого аутентификатора:

# config/packages/security.yaml
security:
    # ...

    firewalls:
        # ...

        main:
            anonymous: lazy
            logout: ~

            guard:
                authenticators:
                    - App\Security\TokenAuthenticator

            # если хотите, отключите сохранение пользователя в сессии
            # stateless: true

            # ...

Вы сделали это! Теперь у вас есть полностью работающая система аутентификации с использованием токена API. Если вашей домашней странице требуется ROLE_USER, вы можете протестировать ее в других условиях:


# тест без токена
curl http://localhost:8000/
# {"message": "Требуется аутентификация"}

# тест с неверным токеном
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
# {"message": "Имя пользователя не найдено."}

# тест с рабочим токеном
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
# выполняется контроллер домашней страницы: страница загружается нормально
 

Теперь узнаем больше о том, что делает каждый метод.

Методы Guard Authenticator

Каждому аутентификатору нужны следующие методы:

supports(Request $request)

Метод вызывается при каждом запросе, и ваша задача состоит в том, чтобы решить, должен ли аутентификатор использоваться для этого запроса (вернуть true) или его следует пропустить (вернуть false).

getCredentials(Request $request)

Ваша задача - прочитать токен (или какую-либо информацию о вашей аутентификации) из запроса и вернуть его. Эти учетные данные передаются в getUser().

getUser($credentials, UserProviderInterface $userProvider)

Аргумент $credentials - это значение, возвращаемое getCredentials(). Ваша задача - вернуть объект, который реализует UserInterface. Если вы это сделаете, то вызывается checkCredentials(). Если вы вернете null (или сгенерируете исключение AuthenticationException), аутентификация завершится неудачно.

checkCredentials ($credentials, UserInterface $user)

Если getUser() возвращает объект User, вызывается этот метод. Ваша задача - проверить правильность учетных данных. Для формы входа в систему вы должны проверить, что пароль правильный для пользователя. Чтобы пройти аутентификацию, верните true. Если вы вернете false (или выбросите AuthenticationException), аутентификация не удастся.

onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)

Метод вызывается после успешной аутентификации, и ваша задача - либо вернуть объект Response, который будет отправлен клиенту, либо NULL для продолжения запроса (например, разрешить вызов маршрута / контроллера, как обычно). Поскольку это API, в котором каждый запрос аутентифицируется, вы хотите вернуть значение null.

onAuthenticationFailure (Request $request, AuthenticationException $exception)

Метод вызывается, если аутентификация не удалась. Ваша задача - вернуть объект Response, который должен быть отправлен клиенту. Исключение $exception скажет вам, что пошло не так во время аутентификации.

start(Request $request, AuthenticationException $authException = null)

Метод вызывается, если клиент обращается к URI / ресурсу, который требует аутентификации, но никакие детали аутентификации не были отправлены. Ваша задача - вернуть объект Response, который помогает пользователю аутентифицироваться (например, ответ 401, который говорит «токен отсутствует!»).

supportsRememberMe()

Если вы хотите поддерживать функцию «запомнить меня», верните true из этого метода. Вам нужно будет активировать remember_me в конфигурации firewall, чтобы это работал. Поскольку это API без сохранения состояния, в этом примере вы не хотите поддерживать функцию «запомнить меня».

createAuthenticatedToken (UserInterface $user, строка $providerKey)

Если вы реализуете AuthenticatorInterface вместо расширения класса AbstractGuardAuthenticator, вы должны реализовать этот метод. Он будет вызван после успешной аутентификации для создания и возврата токена (класса, реализующего GuardTokenInterface) для пользователя, который был указан в качестве первого аргумента.

На рисунке ниже показано, как Symfony вызывает методы Guard Authenticator:

Кастомизация сообщений об ошибках

Когда вызывается onAuthenticationFailure(), ему передается исключение AuthenticationException, которое описывает, как произошла ошибка аутентификации, с помощью метода $exception->getMessageKey()$exception->getMessageData()). Сообщение будет отличаться в зависимости от того, где произошла ошибка аутентификации (то есть getUser() и checkCredentials()).

Но вы также можете вернуть пользовательское сообщение, выбрасывая исключение CustomUserMessageAuthenticationException. Вы можете выбросить это из getCredentials(), getUser() или checkCredentials(), чтобы вызвать сбой:

// src/Security/TokenAuthenticator.php
// ...

use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    // ...

    public function getCredentials(Request $request)
    {
        // ...

        if ($token == 'ILuvAPIs') {
            throw new CustomUserMessageAuthenticationException(
                'Это просто фраза, а не реальный API ключ'
            );
        }

        // ...
    }

    // ...
}

В этом случае, поскольку «ILuvAPIs» является неверным ключом API, вы можете добавить «пасхальное яйцо», чтобы вернуть пользовательское сообщение, если кто-то попытается это сделать:


curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
# {"message":"Это просто фраза, а не реальный API ключ"}

Аутентификация пользователя вручную

Иногда вы можете захотеть вручную аутентифицировать пользователя - например после того, как пользователь завершит регистрацию. Для этого используйте ваш аутентификатор и сервис под названием GuardAuthenticatorHandler:

// src/Controller/RegistrationController.php
// ...

use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request)
    {
        // ...

        // после проверки пользователя и сохранения его в базе данных
        // аутентифицировать пользователя и использовать onAuthenticationSuccess на аутентификаторе
        return $guardHandler->authenticateUserAndHandleSuccess(
            $user,          // ранее созданный объект User, который вы уже сохранили в базе данных
            $request,
            $authenticator, // Аутентификатор, чей onAuthenticationSuccess вы хотите использовать
            'main'          // имя брандмауэра из security.yaml
        );
    }
}

Избегайте аутентификации браузера при каждом запросе

Если вы создаете систему входа с помощью Guard, которая используется браузером, и у вас возникают проблемы с сессией или токенами CSRF, причиной может быть неправильное поведение вашего аутентификатора. Когда средство проверки подлинности Guard предназначено для использования браузером, вам не следует проверять подлинность пользователя при каждом запросе. Другими словами, вам нужно убедиться, что метод support() возвращает true только тогда, когда вам действительно нужно аутентифицировать пользователя. Почему? Поскольку, когда support() возвращает true (и аутентификация в конечном итоге успешна), в целях безопасности сессия пользователя «переносится» на новый идентификатор (сессии).

Это узкий случай и если у вас нет проблем с сессией или токеном CSRF, вы можете игнорировать это. Вот пример хорошего и неправильного поведения:

public function supports(Request $request)
{
    // ХОРОШЕЕ поведение: только проверять подлинность (т.е. возвращать истину) на определенном маршруте
    return 'login_route' === $request->attributes->get('_route') && $request->isMethod('POST');

    // например ваша система авторизации проходит аутентификацию по IP-адресу пользователя
    // НЕПРАВИЛЬНОЕ поведение: Итак, вы решили *всегда* возвращать true, чтобы
    // Вы могли проверить IP-адрес пользователя при каждом запросе
    return true;
}

Проблема возникает, когда ваш browser-based аутентификатор пытается аутентифицировать пользователя при каждом запросе - как в примере на основе IP-адреса выше. Есть два возможных исправления:

  • Если вам не нужно, чтобы аутентификация сохранялась в сеcсии, установите stateless: true для вашего брандмауэра в security.yaml.
  • Обновите свой аутентификатор, чтобы избежать аутентификации, если пользователь уже аутентифицирован:
// src/Security/MyIpAuthenticator.php
// ...

 use Symfony\Component\Security\Core\Security;

class MyIpAuthenticator
{
     private $security;

     public function __construct(Security $security)
     {
         $this->security = $security;
     }

    public function supports(Request $request)
    {
         // если уже есть аутентифицированный пользователь (вероятно, из-за сессии)
         // то верните false и пропустите аутентификацию: в этом нет необходимости.
         if ($this->security->getUser()) {
             return false;
         }

         // пользователь не вошел в систему, поэтому аутентификатор должен продолжить
         return true;
    }
}

Если вы используете автоматическое подключение, Security service будет автоматически передана вашему аутентификатору.

Часто задаваемые вопросы

Могу ли я иметь несколько аутентификаторов?

Да! Но когда вы это сделаете, вам нужно будет выбрать только один аутентификатор для вашей точки входа. Это означает, что вам нужно выбрать, какой метод start() должен быть вызван, когда анонимный пользователь пытается получить доступ к защищенному ресурсу. Для получения дополнительной информации см. Как использовать множественные аутентификаторы .

Могу ли я использовать это с form_login?

Да! form_login - это один из способов аутентификации пользователя, так что вы можете использовать его, а затем добавить один или несколько аутентификаторов. Использование Guard Authenticator не вступает в противоречие с другими способами аутентификации.

Могу ли я использовать это с FOSUserBundle?

Да! На самом деле, FOSUserBundle не управляет безопасностью: он просто предоставляет вам объект User и некоторые маршруты и контроллеры, чтобы помочь с авторизацией, регистрацией, забытым паролем и т. д. Когда вы используете FOSUserBundle, вы обычно используете form_login для фактической аутентификации пользователя. Вы можете продолжить делать это (см. Предыдущий вопрос) или использовать объект User из FOSUserBundle и создать свой собственный аутентификатор (ы) (как в этой статье).