Як завантажувати користувачів безпеки з DB (постачальник сутностей)
Дата оновлення перекладу 2023-06-23
Як завантажувати користувачів безпеки з DB (постачальник сутностей)
Система безпеки Symfony може завантажувати користувачів звідки завгодно, наприклад, із DB через Active Directory або сервер OAuth. Ця стаття покаже вам вам, як завантажувати ваших користувачів із DB через сутність Doctrine.
Вступ
Завантаження користувачів через сутність Doctrine має 2 базові кроки:
- Створіть вашу сутність Користувача
- Сконфігуруйте security.yaml так, щоб він завантажував з вашої сутності .
Після цього, ви можете дізнатися більше про заборону неактивних користувачів , використання користувацького запиту і серіалізації користувача в сесії
1) Створіть вашу сутність Користувача
Перед тим, як ви почнете, спочатку переконайтеся, що ви встановили компонент Security:
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.
Хочете знати більше? Дивіться .
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
в об'єктах другого Користувача не
збігається з якої-небудь причини, то користувач буде виведений із системи з
міркувань безпеки.
Незважаючи на те, що все це відбувається автоматично, існує кілька важливих побічних ефектів.
По-перше, інтерфейс Serializable і його методи serialize()
і unserialize()
були додані, щоб дозволити класу User
бути
серіалізованим у сесії. Це може бути не потрібно, залежно від ваших
налаштувань, але скоріше за все, це гарна ідея. У теорії, серіалізувати потрібно
тільки id
, тому що метод refreshUser()
оновлює користувача за кожним запитом, використовуючи id
(як пояснювалося
вище). Це дає нам "свіжий" об'єкт Користувача.
Symfony також використовує username
, salt
, і password
, щоб
верифікувати, що Користувач не змінився між запитами (вона також
викликає ваш метод AdvancedUserInterface
, якщо ви його реалізуєте).
Невдача серіалізації може призвести до того, що ви будете виходити з системи
за кожним запитом. Якщо ваш користувач реалізує
EquatableInterface,
то замість перевірки цих властивостей, викликається ваш isEqualTo(),
і ви можете перевірити ті властивості, які ви хочете. Якщо ви не розумієте цього,
то ви, найімовірніше, не будете реалізовувати цей інтерфейс або турбуватися
про нього.