Как загружать пользователей безопасности из DB (поставщик сущностей)
Дата обновления перевода 2023-06-29
Как загружать пользователей безопасности из DB (поставщик сущностей)
Система безопасности Symfony может загружать пользователей откуда угодно, например, из DB через Active Directory или сервер OAuth. Эта статья покажет вам, как загружать ваших пользователей из DB через сущность Doctrine.
Вступление
Загрузка пользователей через сущность Doctrine имеет 2 базовых шага:
- Создайте вашу сущность Пользователя
- Сконфигурируйте security.yaml так, чтобы он загружал из вашей сущности
После этого, вы можете узнать больше о запрете неактивных пользователей, использовании пользовательского запроса и сериализации пользователя в сессии
1) Создайте вашу сущность Пользователя
До того, как вы начнёте, для начала убедитесь, что вы установили компонент Безопасность:
1
$ composer require security
Для этой записи, представьте, что у вас уже есть сущность User
со следующими полями: id
, username
, password
, email
и isActive
:
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 91 92 93 94 95 96
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Table(name="app_users")
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", length=64)
*/
private $password;
/**
* @ORM\Column(type="string", length=60, unique=true)
*/
private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
// может не понадобиться, см. раздел о соли ниже
// $this->salt = md5(uniqid('', true));
}
public function getUsername()
{
return $this->username;
}
public function getSalt()
{
// вам *может* понадобиться настоящая соль в зависимости от вашего кодировщика
// см. раздел о соли ниже
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
/** @see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// см. раздел о соли ниже
// $this->salt,
));
}
/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// см. раздел о соли ниже
// $this->salt
) = unserialize($serialized);
}
}
Чтобы немного всё скоратить, некоторые из геттер и сеттер методов не показаны. Но вы можете сгенерировать их вручную или с помощью вашего собственного IDE.
Далее, не забудьте создатьтаблицу DB :
1
$ php bin/console doctrine:migrations:diff
Что это за UserInterface?
До этих пор, это просто обычная сущность. Но для того, чтобы использовать этот класс в системе безопасности, она должна реализовывать UserInterface. Это принуждает класс к тому, чтобы он имел пять следующих методов:
Чтобы узнать больше о каждом из них, смотрите UserInterface.
Caution
Метод eraseCredentials()
предназначен только для того, чтобы очищать
потенциально сохранённые нешифрованные пароли (или схожую аккредитацию).
Будьте осторожны с тем, что удаляете, если ваш класс пользователя также
связан с базой данных, так как изменённый объект скорее всего будет
сохраняться во время запроса.
Что делают методы сериализации и десериализации?
В конце каждого запроса, объект User сериализуется в сессии. В следующем
запросе он десериализуется. Чтобы помочь PHP делать это правильно, вам нужно
реализовать Serializable
. Но вам не нужно сериализовать всё: вам нужно
только несколько полей (те, что показаны выше плюс несколько дополнительных,
если вы добавляли другие важные поля к вашей сущности пользователя). По каждому
запросу, используется id
, чтобы запросить свежий объект User
из DB.
Хотите знать больше? Смотрите Как загружать пользователей безопасности из DB (поставщик сущностей).
2) Сконфигурируйте безопасность так, чтобы она загружала из вашей сущности
Теперь, когда у вас есть сущность User
, реализующая UserInterface
, вам
просто надо сказать системе безопасности Symfony о ней в security.yaml
.
В этом примере, пользователь будет вводить своё имя пользователя и пароль
через базовую аутентификацию HTTP. Symfony запросит сущность User
,
совпадающую с этим именем пользователя и потом проверит парль (больше о
паролях через секунду):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# ...
providers:
our_db_provider:
entity:
class: App\Entity\User
property: username
# если вы используете несколько менеджеров сущностей
# manager_name: customer
firewalls:
main:
pattern: ^/
http_basic: ~
provider: our_db_provider
# ...
Во-первых, раздел encoders
сообщает Symfony ожидать, что пароли в DB
будут зашифрованы с использоваием bcrypt
. Во-вторых, раздел providers
создаёт "поставщика пользователя" под названием our_db_provider
, который
знает, что надо запросить из вашей сущности App\Entity\User
по свойству
username
. Имя our_db_provider
не важно: оно просто должно совпадать со
значением ключа provider
в вашем брандмауэре. Или, если вы не хотите
устанавливать ключ provider
в вашем брандмауэре, то будет автоматически
использован первый "поставщик пользователя".
Создание вашего первого пользователя
Чтобы добавлять пользователей, вы можете реализовать форму регистрации или добавить некоторые fixtures. Это просто обычная сущность, так что нет ничего сложного кроме того, что вам нужно зашифровать пароль каждого пользователя. Но не волнуйтесь, Symfony предоставляет вам сервис, который сделает это за вас. Смотрите Как зашифровать пароль вручную, чтобы узнать детали.
Ниже вы можете увидеть экспорт таблицы app_users
из MySQL с пользователем admin
и паролем admin
(который был зашифрован).
1 2 3 4 5 6
$ mysql> SELECT * FROM app_users;
+----+------------------+--------------------------------------------------------------+--------------------+-----------+
| id | имя пользователя | пароль | email | is_active |
+----+------------------+--------------------------------------------------------------+--------------------+-----------+
| 1 | admin | $2a$08$jHZj/wJfcVKlIwr5AvR78euJxYK7Ku5kURNhNx.7.CSIJ3Pq6LEPC | admin@example.com | 1 |
+----+------------------+--------------------------------------------------------------+--------------------+-----------+
Запрет неактивных пользователей (AdvancedUserInterface)
4.1
Класс AdvancedUserInterface
устарел в Symfony 4.1 и альтернативы предоставлено
не было. Если вам нужен этот функционал в вашем приложении, реализуйте
собственного проверщика пользователя, который
выоплняет необходимые проверки.
Если свойство пользователя isActive
установлено, как false
(т.е.
is_active
равняется 0 в DB), то пользователь всё равно сможет заходить
на сайт нормально. Это легко исправить.
Чтобы исключить неактивных пользователей, измените ваш класс User
,
чтобы он реализовывал AdvancedUserInterface.
Это расширяет UserInterface,
так что вам нужен только новый интерфейс:
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
// src/Entity/User.php
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
// ...
class User implements AdvancedUserInterface, \Serializable
{
// ...
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
// сериализация и десериализация должны быть обновлены - см. ниже
public function serialize()
{
return serialize(array(
// ...
$this->isActive,
));
}
public function unserialize($serialized)
{
list (
// ...
$this->isActive,
) = unserialize($serialized);
}
}
Интерфейс AdvancedUserInterface добавляет четыре дополнительных метода для валидации статуса учётной записи:
- isAccountNonExpired() проверяет, не истёк ли срок годности учётной записи пользователя;
- isAccountNonLocked() проверяет, не закрыт ли пользователь;
- isCredentialsNonExpired() проверяет, не истёк ли срок годности аккредитации пользователя (пароля);
- isEnabled() проверяет, включен ли пользователь.
Если любой из них вернёт false
, ползователю нельзя будет выполнить вход.
Выможете выбрать иметь сохранённые свойства для всех из них, или то, что вам
надо (в этом примере, только isActive
извлекает из DB).
Так в чём разница между методами? Каждый возвращает слегка разные сообщения об ошибке (и они могут быть переведены, когда вы отображаете их в вашем шаблоне входа в систему, чтобы настроить их больше).
Note
Если вы используете AdvancedUserInterface
, вам также надо добавить
любые из свойств, используемых этими методами (как isActive
) к методам
serialize()
и unserialize()
. Если вы не сделаете этого, то ваш
пользователь может быть неправильно десериализован из сессии по каждому
запросу.
Поздравляем! Ваша система безопаности загрузки из DB полностью настроена! Далее, добавьте настоящую форму входа вместо базового HTTP или продолжайте читать о других темах.
Исползование пользовательского запроса для загрузки пользователя
Было бы отлично, если бы пользователь мог выполнять вход с помощью имени пользователя или электронной почты, так как оба уникальны в DB. К сожалению, родной поставщик сущностей способен обрабатывать запросы только через одно свйоство пользователя.
Чтобы сделать это, заставьте ваш UserRepository
реализовывать специальный
UserLoaderInterface. Этот
интерфейс требует только одного метода: loadUserByUsername($username)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Repository/UserRepository.php
namespace App\Repository;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository implements UserLoaderInterface
{
public function loadUserByUsername($username)
{
return $this->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
}
}
Чтобы завершить это, просто удалите ключ property
из поставщика пользователя
в security.yaml
:
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
# ...
providers:
our_db_provider:
entity:
class: App\Entity\User
Это сообщает Symfony не запрашивать Ползователя автоматически. Вместо этого,
когда кто-то выполняет вход, будет вызван метод loadUserByUsername()
в
UserRepository
.
Понимание сериализации и того, как сохраняется пользователь в сессии
Если вас интересует важность метода serialize()
внутри класса User
,
или то, как объект Пользователя сериализуется или десериализуется, то этот
раздел для вас. Если нет - просто пропустите его.
Когда пользователь выполнил вход, весь объект Пользователя сериализуется в
сессию. По следующему запросу, объект Пользователя десериализуется. Потом,
значение свойства id
используется для повторного запроса свежего объекта
пользователя из DB. Наконец, свежий объект Пользователя сравнивается с
десериализованным объектом Пользователя, чтобы убедиться, что они представляют
одного пользователя. Например, если username
в объектах 2 Пользователя не
совпадает по какой-либо причине, то ползователь будет выведен из системы из
соображений безопасности.
Несмотря на то, что всё это происходит автоматически, существует несколько важных побочных эффектов.
Во-первых, интерфейс Serializable и его методы serialize()
и unserialize()
были добавлены, чтобы разрешить классу User
быть
сериализованым в сессии. Это может быть не нужно, в зависимости от ваших
настроек, но скорее всего, это хорошая идея. В теории, сериализовать нужно
только id
, потому что метод refreshUser()
обновляет пользователя по каждому запросу, используя id
(как объяснялось
выше). Это даёт нам "свежий" объект Пользователя.
Ео Symfony также использует username
, salt
, и password
, чтобы
верифицировать, что Пользователь не изменился между запросами (она также
вызывает ваш метод AdvancedUserInterface
, если вы его реализуете).
Неудача сериализации может привести к тому, что вы будете выходить из системы
покаждому запросу. Если ваш ползователь реализует
EquatableInterface,
то вместо проврки этих свойств, вызывается ваш isEqualTo(),
и вы можете проверить те свойства, которые вы хотите. Кроме случаев,
and you can check whatever properties you want. Если вы не понимаете это,
то вы, скорее всего, не будете реализовывать этот интерфейс или беспокоиться
о нём.