Постачальники користувачів Безпеки

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

Постачальники користувачів Безпеки

Постачальники користувачів - це PHP-класи, пов'язані з Безпекою Symfony, які мають два завдання:

Повторно завантажувати користувача з сесії
На початку кожного запиту (окрім випадків, коли ваш брандмауер stateless (без збереження стану)), Symfony завантажує об'єкт User з сесії. Щоб переконатися, що він не застарів, постачальник користувачів "оновлює його". Постачальник користувачів Doctrine, наприклад, робить запит до бази даних за свіжими даними. Symfony потім перевіряє, щоб побачити, чи "змінився" користувач та деаутентифікує користувача, якщо це сталося (див. Постачальники користувачів Безпеки).
Завантажити користувача для деякої функції
Деякі фунеції на кшталт імперсонації користувача, Запам'ятати мене і багатьох вбудованих постачальників аутентифікації, використовують постачальника користувачів для завантаження об'єкта Користувач через його "ім'я користувача" (або електронну пошту чи будь-яке інше поле за вашим бажанням).

Symfony постачається з декількома вбудованими постачальниками користувачів:

Вбудовані постачальники користувачів охоплюють всі потреби для більшості додатків, але ви також можете створити власного користувацького постачальника користувачів.

Постачальник користувачів сутності

Це найбільш розповсюджений постачальник користувачів для традиційних веб-додатків. Користувачі зберігаються у базі даних і постачальник користувачів використовує Doctrine, щоб отримати їх:

  • YAML
  • XML
  • PHP
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:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...

    providers:
        users:
            entity:
                class: App\Entity\User

Це повідомляє Symfony не робити автоматичний запит Користувача. Замість цього, коли буде необхідно (наприклад, через імперсонацію користувача, Запам'ятати мене або активації якоїсь іншої функції безпеки), буде викликаний метод loadUserByIdentifier() у UserRepository.

Постачальник користувачів пам'яті

Не рекомендується використовувати цього постачальника користувачів у реальних додатках через його обмеження та те, наскільки складно управляти користувачами. Він може бути корисний у прототипах додатків і для обмежених додатків, які не зберігають користувачів у базах даних.

Цей постачальник користувачів зберігає всю інформацію користувачів у файлі конфігурації, включно з їх паролями. Тому першим кроком буде конфігурація того, як ці користувачі будуть хешувати свої паролі:

  • YAML
  • XML
  • PHP
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__);
    }

    /**
     * Повідомляє Symfony використовувати цього постачальника для цього класу користувача.
     */
    public function supportsClass(string $class)
    {
        return User::class === $class || is_subclass_of($class, User::class);
    }

    /**
     * Оновлює хешований пароль користувача, зазвичай з використанням кращого алгоритму хешування.
     */
    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'