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

Независимо от того, что вам нужно создать - традиционную форму в хода в систему, систему аутентификации API-токена или интегрировать какую-либо систему собственнси единого-входа, компонент Защита (Guard) может сделать это лёгким... и весёлым!

В этом примере, вы создадите систему аутентификации API-токена и узнаете, как работать с Защитой.

Создайте пользовател и поставщика пользователя

Независимо от того, как вы проводите аутентификацию, вам нужно создать класс Ползователя, реализующий UserInterface и сконфигурировать поставщика пользователя. В этом примере, пользователи хранятся в базе данных через Doctrine, и каждый ползователь имеет свойство apiKey, которое они используют, чтобы получить доступ к своей учётной записи через API:

 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
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

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

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

    public function getUsername()
    {
        return $this->username;
    }

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function getPassword()
    {
    }
    public function getSalt()
    {
    }
    public function eraseCredentials()
    {
    }

    // больше геттеров/сеттеров
}

Tip

Этот Пользователь не имеет пароля, но вы можете добавить свойство password, если вы также хотите позволить этому пользователю выполнять вход с паролем (например, через форму входа в систему).

Ваш класс User не должен храниться в Doctrine: делайте то, что вам нужно. Далее, убедитесь, что вы сконфигурировали "поставщика пользователя" для пользователя:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # app/config/security.yml
    security:
        # ...
    
        providers:
            your_db_provider:
                entity:
                    class: AppBundle:User
                    property: apiKey
    
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <provider name="your_db_provider">
                <entity class="AppBundle:User" />
            </provider>
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'providers' => array(
            'your_db_provider' => array(
                'entity' => array(
                    'class' => 'AppBundle:User',
                ),
            ),
        ),
    
        // ...
    ));
    

Вот и всё! Если вам нужно больше информации об этом шаге, смотрите:

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

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

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

 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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// src/AppBundle/Security/TokenAuthenticator.php
namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    /**
     * Вызывается по каждому запросу. Верните те сертификаты, которые вы
     * хотите передать getUser(). Возвращение "null" приведёт к пропуску
     * аутентификатора.
     */
    public function getCredentials(Request $request)
    {
        if (!$token = $request->headers->get('X-AUTH-TOKEN')) {
            // Нет токена?
            $token = null;
        }

        // То, что вы возвращаете здесь, будет передано getUser() как $credentials
        return array(
            'token' => $token,
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $apiKey = $credentials['token'];

        if (null === $apiKey) {
            return;
        }

        // если null, то аутентификация будет неудачной
        // если объект Пользователя, то вызывается checkCredentials()
        return $userProvider->loadUserByUsername($apiKey);
    }

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

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

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

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $data = array(
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

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

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

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

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

    public function supportsRememberMe()
    {
        return false;
    }
}

Хорошая работа! Каждый метод разъясняется ниже: Методы аутентификатора защиты.

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

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

Наконец, сконфигурируйте ваш ключ firewalls в security.yml, чтобы использовать этот аутентификатор:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            # ...
    
            main:
                anonymous: ~
                logout: ~
    
                guard:
                    authenticators:
                        - AppBundle\Security\TokenAuthenticator
    
                # если вы хотите, отключите хранение пользователей в сессии
                # без фиксации состояния: true
    
                # может другие вещи, как form_login, remember_me, и т.д.
                # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
        <config>
            <!-- ... -->
    
            <firewall name="main"
                pattern="^/"
                anonymous="true"
            >
                <logout />
    
                <guard>
                    <authenticator>AppBundle\Security\TokenAuthenticator</authenticator>
                </guard>
    
                <!-- ... -->
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // app/config/security.php
    
    // ..
    use AppBundle\Security\TokenAuthenticator;
    
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'       => array(
                'pattern'        => '^/',
                'anonymous'      => true,
                'logout'         => true,
                'guard'          => array(
                    'authenticators'  => array(
                        TokenAuthenticator::class
                    ),
                ),
                // ...
            ),
        ),
    ));
    

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# тестировать без токена
curl http://localhost:8000/
# {"message":"Authentication Required"}

# тестировать с плохим токеном
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
# {"message":"Username could not be found."}

# тестировать с рабочим токеном
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
# the homepage controller is executed: the page loads normally

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

Методы аутентификатора защиты

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

getCredentials(Request $request)
Будет вызываться на каждый запрос, и ваша задача - считывать токен (или то, что является вашей информацией "аутентификации) из запроса и возвращатьл его. Если вы вернёте null, то остальной процесс аутентификации будет пропущен. В друних случаях, будет вызван ``getUser()``и возвращённое значение будет передано в качестве первого аргумента.
getUser($credentials, UserProviderInterface $userProvider)
Если getCredentials() возвращает ненулевое значение, то вызывается этот метод, и его значение передаётся, как аругмент $credentials. Ваша задача - вернуть объект, реализующий UserInterface. Если вы это сделаете, то будет вызван checkCredentials(). Если вы вернёте null (или вызовете исключение AuthenticationException), то аутентификация будет неуспешной.
checkCredentials($credentials, UserInterface $user)
Если getUser() возвращает объект Пользователя, то будет вызван этот метод. Ваша задача - убедиться, что сертификаты правильные. В форме входа, именно тут вы будете проверять, правильный ли пароль для этого пользователя. Чтобы пройти аутентификацию, передайте true. Если вы вернёте что-либо другое (или вызовете исключение AuthenticationException), то аутентификация будет неуспешной.
onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)

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

сам, то вы хотите вернуть null.
onAuthenticationFailure(Request $request, AuthenticationException $exception)
Вызывается, если аутентификация неуспешна. Ваша задача - вернуть объект Response, который должен быть отправлен клиенту. $exception сообщит вам о том, что пошло не так во время аутентификации.
start(Request $request, AuthenticationException $authException = null)
Вызывается, если клиент заходит на URI/ресурс, который требует аутнетификации, но детали аутентификации не были отправлены (т.е. вы вернули null из getCredentials()). Ваша задача - вернуть объект Response, который помогает пользователю аутентифицироваться (например, ответ 404, сообщающий "токен отсутствует!").
supportsRememberMe()
Если вы хотите поддержать функцию "запомнить меня", верните true из этого метода. Вам всё равно надо будет активировать remember_me в вашем брандмауэре, чтобы он работал. Так как это API без запоминания состояния, то вы не хотите поддерживать функцию "запомнить меня" в этом примере.
createAuthenticatedToken(UserInterface $user, string $providerKey)
Если вы реализуете GuardAuthenticatorInterface вместо расширения класса AbstractGuardAuthenticator, то вам нужно реализовать этот метод. Он будет вызван после успешной аутентифакции, чтобы создать и вернуть токен для пользователя, который был поставлен в качестве первого аргумента.

Изображение ниже отображает, как Symfony вызывает методы аутентификатора защиты:

_images/security/authentication-guard-methods.png

Настраивание сообщений об ошибке

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/AppBundle/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(
                'ILuvAPIs is not a real API key: it\'s just a silly phrase'
            );
        }

        // ...
    }

    // ...
}

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

1
2
curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
# {"message":"ILuvAPIs - не настоящий API-ключ: это просто глупая фраза"}

Построение формы входа

Если вы строите форму входа, используйте AbstractFormLoginAuthenticator в качестве вашего основного класса - он реализует для вас несколько методов. Потом, заполните другие методы, так же как и с TokenAuthenticator. Вне Защиты, вы всё ещё отвечаете за создание маршрута, контроллера и шаблона для вашей формы входа.

Добавление CSRF-защиты

Если вы используете аутентификатор Защиты, чтобы построить форму входа, и хотите добавить CSRF-защиту - это не проблема!

Для начала, добавьте _csrf_token в ваш шаблон входа.

Потом, типизируйте CsrfTokenManagerInterface в вашем методе __construct() (или вручную сконфигурируйте, чтобы передавался сервис security.csrf.token_manager) и добавьте следующую логику:

 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
// src/AppBundle/Security/ExampleFormAuthenticator.php
// ...

use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenExceptionl

class ExampleFormAuthenticator extends AbstractFormLoginAuthenticator
{
    private $csrfTokenManager;

    public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
    {
        $this->csrfTokenManager = $csrfTokenManager;
    }

    public function getCredentials(Request $request)
    {
        $token = $request->request->get('_csrf_token');

        if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
            throw new InvalidCsrfTokenException('Invalid CSRF token.');
        }

        // ... вся ваша обычная логика
    }

    // ...
}

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

Может ли у меня быть несколько аутентификаторов?
Да! Но если это ваш случай, то вам понадобиться выбрать один из них, чтобы он был вашей точкой входа ("entry_point"). Это означает, что вам понадобится выбрать, какой метод аутентификатора start() должен быть вызван, когда анонимный пользователь пытается получить доступ к защищённому ресурсу. Чтобы узнать больше, смотрите How to Use Multiple Guard Authenticators.
Могу ли я использовать это с form_login?
Да! form_login - это один из способов аутентифицировать пользователя, так что вы можете использовать его и потом добавить один или более аутентификаторов. Использование аутентификатора защиты не конфликтует ни с одним другим способом аутентификации.
Могу ли я использовать это с FOSUserBundle?
Да! На самом деле, FOSUserBundle не работает с безопасностью, он просто предоставляет вам объект User и некоторые маршруты и контроллеры, чтобы помочь с выполнением входа, регистрацией, забытым паролем и т.д. Когда вы используете FOSUserBundle, вы обычно используете form_login для аутентификации пользователя. Вы можете продолжать делать это (смотрите предыдущий вопрос), или использовать объект User их FOSUserBundle и создать ваш собственный authenticator(s) (так же, как в этой статье).

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.