Поставщики пользователей Безопасности
Дата обновления перевода 2023-07-06
Поставщики пользователей Безопасности
Поставщики пользователей - это PHP-классы, связанные с Безопасностью Symfony, которые имеют две задачи:
- Повторно загружать пользователя из сессии
-
В начале каждого запроса (кроме случаев, когда ваш файерволл
stateless
(без сохранения состояния)), Symfony загружает объектUser
из сессии. Чтобы убедиться, что он не устарел, поставщик пользователей "обновляет его". Поставщик пользователей Doctrine, к примеру, делает запрос в базу данных за свежими данными. Symfony затем проверяет, чтобы увидеть, "изменился" ли пользователь и де-аутентифицирует пользователя, если это произошло (см. Поставщики пользователей Безопасности). - Загрузить пользователя для некоторой функции
- Некоторые функции, вроде имперсонации пользователя, Запомнить меня и многих встроенных поставщиков аутентификации, используют поставщика пользователей для загрузки объекта Пользователь через его "имя пользователя" (или электронную почту или любого другого поля по вашему желанию).
Symfony поставляется с несколькими встроенными поставщиками пользователей:
- Поставщик пользователей сущности (загружает пользователей из базы данных);
- Поставщик пользователей LDAP (загружает пользователей с LDAP-сервера);
- Поставщик пользователей памяти (загружает пользователей из файла конфигурации);
- Цепной поставщик пользователей (слияет два или более поставщиков пользователей в нового поставщика пользователей).
Встроенные поставщики пользователей покрывают все потребности для большинства приложений, но вы также можете создать собственного пользовательского поставщика пользователей.
Поставщик пользователей сущности
Это наиболее распространенный постащик пользователей для традиционных веб-приложений. Пользователи хранятся в базе даннхы и поставщик пользователей использует Doctrine, чтобы извлечь их:
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
:
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
# ...
providers:
users:
entity:
class: App\Entity\User
Это сообщает Symfony не делать автоматический запрос Пользователя. Вместо этого,
когда необходим (например, из-за имперсонации пользователя,
Запомнить меня, или активации какой-то другой функции безопасности).
будет вызван метод loadUserByIdentifier()
в UserRepository
.
Поставщик пользователей памяти
Не рекомендуется использовать этого поставщика пользоваталей в реальных приложениях из-за его ограничений и того, насколько сложно управлять пользователями. Он может быть полезен в прототипах приложений и для ограниченных приложений, которые не хранят пользователей в базах данных.
Этот поставщик пользоваталей хранит всю информацию пользоваталей в файле конфигурации, включая их пароли. Поэтому первым шагом будет конфигурация того, как эти пользователи будут хешировать свои пароли:
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'