Поставщики пользователей Безопасности

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

Поставщики пользователей Безопасности

Поставщики пользователей - это PHP-классы, связанные с Безопасностью Symfony, которые имеют две задачи:

Повторно загружать пользователя из сессии
В начале каждого запроса (кроме случаев, когда ваш файерволл stateless (без сохранения состояния)), Symfony загружает объект User из сессии. Чтобы убедиться, что он не устарел, поставщик пользователей "обновляет его". Поставщик пользователей Doctrine, к примеру, делает запрос в базу данных за свежими данными. Symfony затем проверяет, чтобы увидеть, "изменился" ли пользователь и де-аутентифицирует пользователя, если это произошло (см. Поставщики пользователей Безопасности).
Загрузить пользователя для некоторой функции
Некоторые функции, вроде имперсонации пользователя, Запомнить меня и многих встроенных поставщиков аутентификации, используют поставщика пользователей для загрузки объекта Пользователь через его "имя пользователя" (или электронную почту или любого другого поля по вашему желанию).

Symfony поставляется с несколькими встроенными поставщиками пользователей:

Встроенные поставщики пользователей покрывают все потребности для большинства приложений, но вы также можете создать собственного пользовательского поставщика пользователей.

Поставщик пользователей сущности

Это наиболее распространенный постащик пользователей для традиционных веб-приложений. Пользователи хранятся в базе даннхы и поставщик пользователей использует Doctrine, чтобы извлечь их:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# config/packages/security.yaml
security:
    # ...

    providers:
        users:
            entity:
                # класс сущности, представляющей пользователей
                class: 'App\Entity\User'
                # свойство для запроса - например, имя пользователя, электронная почта и т.д.
                property: 'username'
                # необязательно: если вы используете несколько менеджеров сущностей Doctrine,
                # эта опция определяет, какого использовать
                # manager_name: 'customer'

    # ...

Раздел providers создает "user provider" под названием users, который знает, как делать запрос из вашей сущности App\Entity\User по свойству username. Вы можете выбрать любое название для поставщика пользователей, но рекомендуется выбирать описательное название, так как оно будет позже использоваться в конфигурации файерволла.

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

Поставщик entity может делать запрос только из одного конкретного поля, указаннного ключом конфигурации property. Если вы хотите иметь немного больше контроля - например, вы хотите найти пользователя по email или username, вы можете сделать это заставив ваш UserRepository реализовывать UserLoaderInterface. Этот интерфейс требует только одного метода: loadUserByIdentifier($identifier):

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/Repository/UserRepository.php
namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;

class UserRepository extends ServiceEntityRepository implements UserLoaderInterface
{
    // ...

    // Метод loadUserByIdentifier() был представлен в Symfony 5.3.
    // В предыдущих версиях он назывался loadUserByUsername()
    public function loadUserByIdentifier(string $usernameOrEmail): ?User
    {
        $entityManager = $this->getEntityManager();

        return $entityManager->createQuery(
                'SELECT u
                FROM App\Entity\User u
                WHERE u.username = :query
                OR u.email = :query'
            )
            ->setParameter('query', $usernameOrEmail)
            ->getOneOrNullResult();
    }
}

Чтобы закончить это, удалите ключ property из поставщика пользователей в security.yaml:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...

    providers:
        users:
            entity:
                class: App\Entity\User

Это сообщает Symfony не делать автоматический запрос Пользователя. Вместо этого, когда необходим (например, из-за имперсонации пользователя, Запомнить меня, или активации какой-то другой функции безопасности). будет вызван метод loadUserByIdentifier() в UserRepository.

Поставщик пользователей памяти

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

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

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...
    password_hashers:
        # этот внутренний класс используется Symfony для представления пользователей в памяти
        # (класс 'InMemoryUser' был представлен в Symfony 5.3.
        # В предыдущих версиях он назывался 'User')
        Symfony\Component\Security\Core\User\InMemoryUser: 'auto'

Затем, выполните эту команду, чтобы хешировать текстовые пароли ваших пользователей:

1
$ php bin/console security:hash-password

Теперь вы можете сконфигурировать всю информацию пользователей в config/packages/security.yaml:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    # ...
    providers:
        backend_users:
            memory:
                users:
                    john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] }
                    jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] }

Caution

При использовании поставщика memory, а не auto-алгоритма, вам нужно выбирать алгоритм хеширования без соли (т.е. bcrypt).

Поставщик пользователей LDAP

Этот поставщик пользователей требует установки определенных зависимостей и использования некоторых специальных поставщиков аутентификации, поэтому это разъясняется в отдельной статье: Аутентификация с LDAP-сервером.

Цепной поставщик пользователей

Этот поставщик пользователей объединяет два или боле других типов постащиков пользователей (entity, memory и ldap), чтобы создать нового поставщика пользователей. Порядок, в котором сконфигурированы поставщики, важен, так как Symfony будет искать пользователей начиная с первого поставщика и будет продолжать искать их в других поставщиков, пока не найдет:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# config/packages/security.yaml
security:
    # ...
    providers:
        backend_users:
            memory:
                # ...

        legacy_users:
            entity:
                # ...

        users:
            entity:
                # ...

        all_users:
            chain:
                providers: ['legacy_users', 'users', 'backend_users']

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

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

Для начала, убедитесь, что вы следовали Руководству Безопасности при создании вашего класса User.

Если вы использовали команду make:user для создания вашего класса User (и вы ответили на вопросы, указывающие, что вам необходим пользовательский поставщик), то эта команда сгенерирует хороший костяк для того, чтобы начать:

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

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
    /**
     * Метод loadUserByIdentifier() был представлен в Symfony 5.3.
     * В предыдущих версиях он назывался loadUserByUsername()
     *
     * Symfony вызывает этот метод, если вы используете функции вроде switch_user
     * или remember_me. Если вы не используете эти функции, вам не нужно реализовывать
     * этот метод.
     *
     * @throws UserNotFoundException if the user is not found
     */
    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        // Загрузить объект User из вашего источника данных или вызвать UserNotFoundException.
        // Аргумент $identifier - то значение, которое возвращается методом
        // getUserIdentifier() в вашем классе User.
        throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__);
    }

    /**
     * Обновляет пользователя после повторной загрузки из сессии.
     *
     * Когда пользователь вошел в систему, в начале каждого запроса, объект
     * User загружается из сессии, а затем вызывается этот метод. Ваша задача
     * - убедиться, что данные пользователя все еще свежие, путем, к примеру,
     * повторного запроса свежих данных пользователя.
     *
     * Если ваш файерволл "stateless: true" (для чистого API), этот метод
     * не вызывается.
     *
     * @return UserInterface
     */
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
        }

        // Вернуть объект User после того, как убедились, что его данные "свежие".
        // Или вызвать UserNotFoundException, если пользователь уже не существует.
        throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
    }

    /**
     * Tells Symfony to use this provider for this User class.
     */
    public function supportsClass(string $class)
    {
        return User::class === $class || is_subclass_of($class, User::class);
    }

    /**
     * Upgrades the hashed password of a user, typically for using a better hash algorithm.
     */
    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    {
        // СДЕЛАТЬ: когда используются хешированные пароли, этот метод должен:
        // 1. сохранять новый пароль в хранилище пользователя
        // 2. обновлять объект $user с $user->setPassword($newHashedPassword);
    }
}

Большинство работы уже сделано! Прочтите комментарии в коде и обновите разделы СДЕЛАТЬ, чтобы закончить с поставщиком пользователей. Когда вы закончите, сообщите Symfony о поставщике пользователей, добавив его в security.yaml:

1
2
3
4
5
6
# config/packages/security.yaml
security:
    providers:
        # имя вашего поставщика пользователя может быть любым
        your_custom_user_provider:
            id: App\Security\UserProvider

Наконец, обновите файл config/packages/security.yaml, чтобы установить ключ provider как your_custom_user_provider во всех файерволлах, которые будут использовать этого пользовательского поставщика.

Понимание того, как обновляются пользователи из сессии

В конце каждого запроса (кроме случаев, когда ваш файерволл - stateless), ваш объект User сериализуется в сессии. В начале следующего запроса, он десериализуется и затем передается вашему поставщику пользователей для "обновления" (например, Doctrine делает запрос свежего пользователя).

Затем, два объекта Пользователя (первоначальный из сессии и обновленный объект Пользователя) "сравниваются", чтобы увидеть "равны" ли они. По умолчанию, базовый класс AbstractToken сравнивает возвращенные значения методов getPassword(), getSalt() и getUserIdentifier(). Если какие-либо из них отличаются, пользователь будет выведен из системы. Это мера безопасности, чтобы гарантировать де-аутентификацию злоумышленных пользователей в случае изменения базовых данных пользователя.

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

В этом случае, пересмотрите логику сериализации (например, SerializableInterface), если она у вас есть, чтобы убедиться, что сериализуются все необходимые поля.

Сравнение пользователей вручную с помощью EquatableInterface

Или, если вам нужно больше контроля над процессом "сравнения пользователей", сделайте так, чтобы ваш класс User реализовывал EquatableInterface. Затем, ваш метод isEqualTo() будет вызыван при сравнении пользователей.

Внедрение поставщика пользователей в ваши сервисы

Symfony определяет несколько сервисов, связанных с поставщиками пользователей:

1
2
3
4
5
6
7
$ php bin/console debug:container user.provider

  Выберите один из следующих сервисов для отображения его информации:
  [0] security.user.provider.in_memory
  [1] security.user.provider.ldap
  [2] security.user.provider.chain
  ...

Большинство из этих сервисов абстрактны и не могут быть внедрены в ваши сервисы. Вместо этого, вы должны внедрить обычный сервис, который Symfony создает для каждого из ваших поставщиков пользователей. Названия этих сервисов следуют такому паттерну: security.user.provider.concrete.<your-provider-name>.

Например, если вы создаете форму входа в систему и хотите внедрить в ваш LoginFormAuthenticator поставщика пользователей типа memory и вызвали backend_users, сделайте следующее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Security/LoginFormAuthenticator.php
namespace App\Security;

use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    private $userProvider;

    // измените подсказку 'InMemoryUserProvider' в конструкторе, если
    // вы внедряете другой тип поставщика пользоателей
    public function __construct(InMemoryUserProvider $userProvider, /* ... */)
    {
        $this->userProvider = $userProvider;
        // ...
    }
}

Затем, внедрите конкретный сервис, созданный Symfony для поставщика пользователей backend_users:

1
2
3
4
5
6
# config/services.yaml
services:
    # ...

    App\Security\LoginFormAuthenticator:
        $userProvider: '@security.user.provider.concrete.backend_users'