Как загружать пользователей безопасности из БД (поставщик сущностей)

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

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.

Далее, не забудьте создатьтаблицу БД:

1
$ php bin/console doctrine:migrations:diff

Что это за UserInterface?

До этих пор, это просто обычная сущность. Но для того, чтобы использовать этот класс в системе безопасности, она должна реализовывать UserInterface. Это принуждает класс к тому, чтобы он имел пять следующих методов:

Чтобы узнать больше о каждом из них, смотрите UserInterface.

Caution

Метод eraseCredentials() предназначен только для того, чтобы очищать потенциально сохранённые нешифрованные пароли (или схожую аккредитацию). Будьте осторожны с тем, что удаляете, если ваш класс пользователя также связан с базой данных, так как изменённый объект скорее всего будет сохраняться во время запроса.

Что делают методы сериализации и десериализации?

В конце каждого запроса, объект User сериализуется в сессии. В следующем запросе он десериализуется. Чтобы помочь PHP делать это правильно, вам нужно реализовать Serializable. Но вам не нужно сериализовать всё: вам нужно только несколько полей (те, что показаны выше плюс несколько дополнительных, если вы добавляли другие важные поля к вашей сущности пользователя). По каждому запросу, используется id, чтобы запросить свежий объект User из БД.

Хотите знать больше? Смотрите Understanding serialize and how a User is Saved in the Session.

2) Сконфигурируйте безопасность так, чтобы она загружала из вашей сущности

Теперь, когда у вас есть сущность User, реализующая UserInterface, вам просто надо сказать системе безопасности Symfony о ней в security.yaml.

В этом примере, пользователь будет вводить своё имя пользователя и пароль через базовую аутентификацию HTTP. Symfony запросит сущность User, совпадающую с этим именем пользователя и потом проверит парль (больше о паролях через секунду):

  • YAML
     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
    
        # ...
    
  • XML
     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
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <encoder class="App\Entity\User" algorithm="bcrypt" />
    
            <!-- ... -->
    
            <provider name="our_db_provider">
                <!-- если вы используете несколько менеджеров сущностей, добавьте:
                     manager-name="customer" -->
                <entity class="App\Entity\User" property="username" />
            </provider>
    
            <firewall name="main" pattern="^/" provider="our_db_provider">
                <http-basic />
            </firewall>
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
     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
    // config/packages/security.php
    use App\Entity\User;
    
    $container->loadFromExtension('security', array(
        'encoders' => array(
            User::class => array(
                'algorithm' => 'bcrypt',
            ),
        ),
    
        // ...
    
        'providers' => array(
            'our_db_provider' => array(
                'entity' => array(
                    'class'    => User::class,
                    'property' => 'username',
                ),
            ),
        ),
        'firewalls' => array(
            'main' => array(
                'pattern'    => '^/',
                'http_basic' => null,
                'provider'   => 'our_db_provider',
            ),
        ),
    
        // ...
    ));
    

Во-первых, раздел encoders сообщает Symfony ожидать, что пароли в БД будут зашифрованы с использоваием bcrypt. Во-вторых, раздел providers создаёт "поставщика пользователя" под названием our_db_provider, который знает, что надо запросить из вашей сущности App\Entity\User по свойству username. Имя our_db_provider не важно: оно просто должно совпадать со значением ключа provider в вашем брандмауэре. Или, если вы не хотите устанавливать ключ provider в вашем брандмауэре, то будет автоматически использован первый "поставщик пользователя".

Создание вашего первого пользователя

Чтобы добавлять пользователей, вы можете реализовать форму регистрации или добавить некоторые fixtures. Это просто обычная сущность, так что нет ничего сложного кроме того, что вам нужно зашифровать пароль каждого пользователя. Но не волнуйтесь, Symfony предоставляет вам сервис, который сделает это за вас. Смотрите How to Manually Encode a Password, чтобы узнать детали.

Ниже вы можете увидеть экспорт таблицы 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 | [email protected]  |         1 |
+----+------------------+--------------------------------------------------------------+--------------------+-----------+

Если вы используете bcrypt или argon2i, то нет. В других случаях - да. Все пароли должны быть хешированы с солью, но bcrypt и argon2i делают это внутренне. Так как этот туториал использует bcrypt, метод getSalt() в User может просто вернуть null (не используется). Если вы используете другой алгоритм, вам нужно будет убрать комментарии из строк salt в сущности User и добавить сохранённое свойство salt.

Запрет неактивных пользователей (AdvancedUserInterface)

New in version 4.1: Класс AdvancedUserInterface устарел в Symfony 4.1 и альтернативы предоставлено не было. Если вам нужен этот функционал в вашем приложении, реализуйте собственного проверщика пользователя, который выоплняет необходимые проверки.

Если свойство пользователя isActive установлено, как false (т.е. is_active равняется 0 в БД), то пользователь всё равно сможет заходить на сайт нормально. Это легко исправить.

Чтобы исключить неактивных пользователей, измените ваш класс 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 извлекает из БД).

Так в чём разница между методами? Каждый возвращает слегка разные сообщения об ошибке (и они могут быть переведены, когда вы отображаете их в вашем шаблоне входа в систему, чтобы настроить их больше).

Note

Если вы используете AdvancedUserInterface, вам также надо добавить любые из свойств, используемых этими методами (как isActive) к методам serialize() и unserialize(). Если вы не сделаете этого, то ваш пользователь может быть неправильно десериализован из сессии по каждому запросу.

Поздравляем! Ваша система безопаности загрузки из БД полностью настроена! Далее, добавьте настоящую форму входа вместо базового HTTP или продолжайте читать о других темах.

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

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

Чтобы сделать это, заставьте ваш 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:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # config/packages/security.yaml
    security:
        # ...
    
        providers:
            our_db_provider:
                entity:
                    class: App\Entity\User
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <provider name="our_db_provider">
                <entity class="App\Entity\User" />
            </provider>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // config/packages/security.php
    use App\Entity\User;
    
    $container->loadFromExtension('security', array(
        // ...
    
        'providers' => array(
            'our_db_provider' => array(
                'entity' => array(
                    'class' => User::class,
                ),
            ),
        ),
    ));
    

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

Понимание сериализации и того, как сохраняется пользователь в сессии

Если вас интересует важность метода serialize() внутри класса User, или то, как объект Пользователя сериализуется или десериализуется, то этот раздел для вас. Если нет - просто пропустите его.

Когда пользователь выполнил вход, весь объект Пользователя сериализуется в сессию. По следующему запросу, объект Пользователя десериализуется. Потом, значение свойства id используется для повторного запроса свежего объекта пользователя из БД. Наконец, свежий объект Пользователя сравнивается с десериализованным объектом Пользователя, чтобы убедиться, что они представляют одного пользователя. Например, если username в объектах 2 Пользователя не совпадает по какой-либо причине, то ползователь будет выведен из системы из соображений безопасности.

Несмотря на то, что всё это происходит автоматически, существует несколько важных побочных эффектов.

Во-первых, интерфейс Serializable и его методы serialize() и unserialize() были добавлены, чтобы разрешить классу User быть сериализованым в сессии. Это может быть не нужно, в зависимости от ваших настроек, но скорее всего, это хорошая идея. В теории, сериализовать нужно только id, потому что метод refreshUser() обновляет пользователя по каждому запросу, используя id (как объяснялось выше). Это даёт нам "свежий" объект Пользователя.

Ео Symfony также использует username, salt, и password, чтобы верифицировать, что Пользователь не изменился между запросами (она также вызывает ваш метод AdvancedUserInterface, если вы его реализуете). Неудача сериализации может привести к тому, что вы будете выходить из системы покаждому запросу. Если ваш ползователь реализует EquatableInterface, то вместо проврки этих свойств, вызывается ваш isEqualTo(), и вы можете проверить те свойства, которые вы хотите. Кроме случаев, and you can check whatever properties you want. Если вы не понимаете это, то вы, скорее всего, не будете реализовывать этот интерфейс или беспокоиться о нём.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.