Як завантажувати користувачів безпеки з DB (постачальник сутностей)

Дата оновлення перекладу 2023-06-23

Як завантажувати користувачів безпеки з DB (постачальник сутностей)

Система безпеки Symfony може завантажувати користувачів звідки завгодно, наприклад, із DB через Active Directory або сервер OAuth. Ця стаття покаже вам вам, як завантажувати ваших користувачів із DB через сутність Doctrine.

Вступ

Завантаження користувачів через сутність Doctrine має 2 базові кроки:

  1. Створіть вашу сутність Користувача
  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 |
+----+------------------+--------------------------------------------------------------+--------------------+-----------+

Якщо ви використовуєте bcrypt або argon2i, то ні. В інших випадках - так. Усі паролі мають бути хешовані з сіллю, але bcrypt і argon2i роблять це внутрішньо. Оскільки цей туторіал використовує bcrypt, метод getSalt() в User може просто повернути null (не використовується). Якщо ви використовуєте інший алгоритм, вам потрібно буде прибрати коментарі з рядків salt в сутності User і додати збережену властивість salt.

Заборона неактивних користувачів (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(), і ви можете перевірити ті властивості, які ви хочете. Якщо ви не розумієте цього, то ви, найімовірніше, не будете реалізовувати цей інтерфейс або турбуватися про нього.