Security

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

Security

Symfony надає багато інструментів для безпеки вашого додатку. Деякі інструменти безпеки, пов'язані з HTTP, на кшталт кукі безпечних сесій і CSRF-захисту надаються за замовчуванням. SecurityBundle, про який ви дізнаєтеся у цьому керівництві, надає всі необхідні функції аутентифікації та авторизації для безпеки вашого додатку.

Щоб почати, встановіть SecurityBundle:

1
$ composer require symfony/security-bundle

Якщо у вас встановлено Symfony Flex , він також створить для вас файл конфігурації security.yaml:

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
# config/packages/security.yaml
security:
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#c-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users_in_memory: { memory: null }
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_in_memory

            # активуйте різні способи аутентифікації
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Простий спосіб контролювати доступ до великих розділів вашого сайту
    # Примітка: Буде використаний лише *перший* контроль доступу, що співпадає
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

Це багато конфігурації! У наступних розділах обговорюються три основних елементи:

Користувач (providers)
Будь-якому захищеному розділу вашого додатку потрібен деякий концепт користувача. Постачальник користувачів завантажує користувачів з будь-якого користувача. Постачальник користувачів завантажує користувачів з будь-якого сховища (наприклад, бази даних), засновуючись на "ідентифікаторі користувача" (наприклад, адресі електронної пошти користувача);
Брандмауер та Аутентифікація користувачів (firewalls)
Брандмауер - це основа безпеки вашого додатку. Кожний запит в рамках брандмауера перевіряється на необхідність аутентифікації користувача. Брандмауер також піклується про аутентифікацію цього користувача (наприклад, використовуючи форму входу);
Контроль доступу (Авторизація) (access_control)
Використовуючи контроль доступу та перевірник авторизації, ви контролюєте необхідні дозволи для виконання конкретної дії або відвідування конкретного URL.

Користувач

Дозволи в Symfony завжди пов'язані з об'єктом користувача. Якщо вам потрібно захистити ваш застосунок (або його частини), вам потрібно створити клас користувача. Цей клас реалізує UserInterface. Він часто є сутністю Doctrine, але ви також можете використати відповідний клас користувача Безпеки.

Найпростіший спосіб згенерувати клас користувача - використати команду make:user з MakerBundle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ php bin/console make:user
 Ім'я захищеного класу користувача (наприклад, Користувач) [User]:
 > User

 Ви хочете зберігати дані користувача у базі даних? (через Doctrine)? (так/ні) [yes]:
 > yes

 Введіть ім'я властивості, яка буде унікальним "відображуваним" іменем користувача (наприклад, адресу пошті, ім'я користувача, uuid) [email]:
 > email

 Чи потрібно буде цьому додатку хешувати/перевіряти паролі користувача? Оберіть Ні, якщо паролі не потрібні або будуть перевірені/хешовані якоюсь іншою системою (наприклад, сервером єдиного входу).

 Чи потрібно цьому додатку хешувати/перевіряти пароли користувачів? (так/ні) [yes]:
 > yes

 created: src/Entity/User.php
 created: src/Repository/UserRepository.php
 updated: src/Entity/User.php
 updated: config/packages/security.yaml
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
97
98
99
100
101
102
103
104
105
106
// src/Entity/User.php
namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private int $id;

    #[ORM\Column(type: 'string', length: 180, unique: true)]
    private ?string $email;

    #[ORM\Column(type: 'json')]
    private array $roles = [];

    #[ORM\Column(type: 'string')]
    private string $password;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Публічне уявлення користувача (наприклад, ім'я користувача, адресу пошти і т.д.)
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // гарантувати, що у кожного користувача є хоча б ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Повернення солі лише за необхідності, якщо ви не використовуєте сучасний
     * алгоритм хешування (наприклад, bcrypt або sodium) у вашому security.yaml.
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    {
        return null;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // Якщо ви зберігаєте будь-які тимчасові чутливі дані користувача, очистіть їх тут
        // $this->plainPassword = null;
    }
}

Tip

Починаючи з MakerBundle: v1.57.0 - Ви можете передати --with-uuid або
--with-ulid до make:user. Використовуючи Компонент Uid Symfony, це згенерує сутність User з типом id як Uuid або Ulid замість int.

Якщо ваш користувач є сутністю Doctrine, як у прикладі вище, не забудьте створити таблиці, створивши та запустивши міграцію :

1
2
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate

Tip

Починаючи з MakerBundle: v1.56.0 - Передача --formatted до make:migration
генерує гарний та охайний файл міграції.

Завантаження користувача: Постачальник користувачів

Окрім створення сутності, команда make:user також додає конфігурацію для постачальника користувачів у вашу конфігурацію безпеки:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    # ...

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

Цей постачальник користувачів знає, як (пере)завантажувати користувачів зі сховища (наприклад, бази даних), засновуючись на "ідентифікаторі користувача" (наприклад, адресі електронної пошти або імені користувача). Конфігурація вище використовує Doctrine для завантаження сутності User, використовуючи властивість email як "user identifier".

Постачальники користувачів використовуються у декількох місцях під час життєвого циклу безпеки:

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

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

Постачальник користувачів сутності
Завантажує користувачів з бази даних, використовуючи Doctrine;
Постачальник користувачів LDAP
Завантажує користувачів з LDAP-сервера;
Постачальник користувачів пам'яті
Завантажує користувачів з файлу конфігурації;
Ланцюжковий постачальник користувачів
Злияє два або більше постачальника користувачів у нового постачальника користувачів.

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

Note

Іноді вам може знадобитися впровадити постачальника користувачів в інший клас (наприклад, у ваш користувацький аутентифікатор). Всі постачальники користувачів слідують цьому патерну для своїх ID сервісів: security.user.provider.concrete.<your-provider-name> (де <your-provider-name> - ключ конфігурації, наприклад, app_user_provider). Якщо у вас лише один постачальник користувачів, ви можете автомонтувати його, використовуючи підказку UserProviderInterface.

Реєстрація користувача: Хешування паролів

Багато додатків вимагають входу в систему за допомогою пароля. Для таких додатків SecurityBundle надає хешування паролів та верифікацію функціональності.

Спочатку переконайтеся, що ваш клас User реалізує PasswordAuthenticatedUserInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Entity/User.php

// ...
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // ...

    /**
     * @return string the hashed password for this user
     */
    public function getPassword(): string
    {
        return $this->password;
    }
}

Потім, сконфігуруйте, який хешувальник паролів має бути використаний для цього класу користувача. Якщо ваш файл security.yaml ще не було попередньо сконфігуровано, то make:user повинен був зробити це за вас:

1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    # ...
    password_hashers:
        # Використовувати нативний хешувальник паролів, який автоматично обирає та мігрує кращий
        # можливий алгоритм хешування (починаючи з Symfony 5.3 - це "bcrypt")
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

Тепер, коли Symfony знає як ви хочете хешувати паролі, ви можете використати сервіс UserPasswordHasherInterface, щоб робити це до збереження ваших користувачів у базу даних:

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
// src/Controller/RegistrationController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class RegistrationController extends AbstractController
{
    public function index(UserPasswordHasherInterface $passwordHasher): Response
    {
        // ... наприклад, отримати дані користувача з форми реєстрації
        $user = new User(...);
        $plaintextPassword = ...;

        // хешувати пароль (засновуючись на конфігурації security.yaml для класу $user)
        $hashedPassword = $passwordHasher->hashPassword(
            $user,
            $plaintextPassword
        );
        $user->setPassword($hashedPassword);

        // ...
    }
}

Note

Якщо ваш клас користувача є сутністю Doctrine і ви хешуєте паролі користувачів, то клас репозиторію
клас сховища Doctrine, пов'язаний з користувацьким класом, повинен реалізувати PasswordUpgraderInterface.

Команда-мейкер make:registration-form може допомогти вам налаштувати контролер реєстрації і додати функції на кшталт верифікації адреси електронної пошти, використовуючи SymfonyCastsVerifyEmailBundle.

1
2
$ composer require symfonycasts/verify-email-bundle
$ php bin/console make:registration-form

Ви також можете вручну хешувати пароль, виконавши:

1
$ php bin/console security:hash-password

Прочитайте більше про всі доступні хешувальники і міграції паролів у Хешування та верифікація паролів.

Брандмауер

Розділ firewalls у config/packages/security.yaml - це найважливіший розділ. "Брандмауер" - це ваша система аутентифікації: брандмауер визначає, які частини вашого додатку захищені, і як ваші користувачі будуть проходити аутентифікацію (наприклад, форма входу, API-токен і т.д.).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# config/packages/security.yaml
security:
    # ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_in_memory

            # активувати різні способи аутентифікації
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

За одним запитом активний лише один брандмауер: Symfony використовує ключ pattern, щоб знайти перше співпадіння (ви також можете шукати співпадіння за хостингом або іншим речам).

Брандмауер dev насправді несправжній: він гарантує, що ви випадково не заблокуєте інструменти розробки Symfony, які живуть за URL на кшталт /_profiler і /_wdt.

Tip

При зіставленні декількох маршрутів, замість створення довгого регулярного виразу, ви також можете використовувати масив простіших регулярних виразів для зіставлення з кожним маршрутом:

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/security.yaml
security:
    # ...
    firewalls:
        dev:
            pattern:
                - ^/_profiler/
                - ^/_wdt/
                - ^/css/
                - ^/images/
                - ^/js/
# ...

Ця функція не підтримується у форматі конфігурації XML.

Всі справжні URL обробляються брандмауером main (відсутність ключа pattern означає, що співпадають всі URL). Брандмауер може мати багато режимів аутентифікації, іншими словами - багато способів поставити запитання "Ти хто?".

Часто користувач невідомий (тобто не виконав вхід у систему), коли він вперше потрапляє на ваш сайт. Якщо ви відвідаєте свою домашню сторінку прямо зараз, у вас буде доступ, і ви побачите, що ви відвідуєте сторінку за брандмауером у панелі інструментів:

Панель інструментів профілювальника Symfony, де інформація про безпеку показує «Authenticated: no» і «Firewall name: main»

Посвята URL під іншим брандмауером не обов'язково вимагає вашої аутентифікації (наприклад, форма входу має бути доступною, або деякі частини вашого додатку публічні). Ви дізнаєтеся як обмежувати доступ за URL, контролерам або чомусь ще у вашому брандмауері в розділі контроль доступу .

Ви дізнаєтеся, як обмежити доступ до URL-адрес, контролерів або до будь-чого іншого у вашому брандмауері у розділі контроль доступу .

Tip

Анонімний режим lazy запобігає сесію від запуску, якщо немає необхідності в авторизації (наприклад, чіткій перевірці привілеїв користувача). Важливо залишати запити кешованими (див. HTTP-кеш).

Note

Якщо ви не бачите панелі інструментів, встановіть профільувальник:

1
$ composer require --dev symfony/profiler-pack

Отримання конфігурації брандмауера для запиту

Якщо вам потрібно отримати конфігурацію брандмауера, який співпав із заданим запитом, використайте сервіс Security:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Service/ExampleService.php
// ...

use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;

class ExampleService
{
    public function __construct(
        // Уникайте виклику getFirewallConfig() у конструкторі: аутентифікація може ще
        // бути не завершеною. Натомість, зберігайте весь обʼєкт Security.
        private Security $security,
        private RequestStack $requestStack,
    ) {
    }

    public function someMethod(): void
    {
        $request = $this->requestStack->getCurrentRequest();
        $firewallName = $this->security->getFirewallConfig($request)?->getName();

        // ...
    }
}

Аутентифікація користувачів

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

Tip

Якщо ваш застосунок пропускає користувачів у систему за допомогою сторонніх сервісів, на кшталт Google, Facebook або Twitter (соціальний вхід), розгляньте суспільний пакет HWIOAuthBundle.

Форма входу

Більшість сайтів мають форму входу, де користувачі проходять аутентифікацію, використовуючи ідентифікатор (наприклад, адресу пошти або ім'я користувача) і пароль. Цей функціонал надано аутентифікатором форми входу.

Ви можете виконати наступну команду, щоб створити все необхідне для додавання форми входу у вашому додатку:

1
$ php bin/console make:security:form-login

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

Спочатку створіть контролер для форми входу:

1
2
3
4
$ php bin/console make:controller Login

 created: src/Controller/LoginController.php
 created: templates/login/index.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/LoginController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class LoginController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function index(): Response
    {
        return $this->render('login/index.html.twig', [
            'controller_name' => 'LoginController',
        ]);
    }
}

Потім, підключіть аутентифікатор форми входу, використовуючи налаштування form_login:

1
2
3
4
5
6
7
8
9
10
11
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            form_login:
                # "login" - це ім'я раніше створеного маршруту
                login_path: login
                check_path: login

Note

login_path і check_path підтримують URL і імена маршрутів (але не можуть мати обов'язкових заповнювачів - наприклад, /login/{foo}, де foo не має значення за замовчуванням).

Після підключення, система безпеки перенаправляє неаутентифікованих відвідувачів на login_path, якщо вони спробують потрапити у захищене місце (цю поведінку можна налаштувати, використовуючи точки входу аутентифікації ).

Відредагуйте контролер входу, щоб відобразити форму входу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...
+ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

  class LoginController extends AbstractController
  {
      #[Route('/login', name: 'app_login')]
-     public function index(): Response
+     public function index(AuthenticationUtils $authenticationUtils): Response
      {
+         // отримати помилку входу, якщо вона є
+         $error = $authenticationUtils->getLastAuthenticationError();
+
+         // останнє ім'я користувача, введене користувачем
+         $lastUsername = $authenticationUtils->getLastUsername();
+
          return $this->render('login/index.html.twig', [
-             'controller_name' => 'LoginController',
+             'last_username' => $lastUsername,
+             'error'         => $error,
          ]);
      }
  }

Не дозволяйте цьому контролеру заплутати вас. Його робота - лише відображати форму: аутентифікатор form_login потрубується про відправку форми автоматично. Якщо користувач відправляє невалідну адресу пошти або пароль, цей аутентифікатор збереже помилку та перенаправить назад до цього контролера, де ми прочитаємо помилку (використовуючи AuthenticationUtils), щоб вона могла бути відображена користувачу.

Нарешті, створіть або оновіть шаблон:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{# templates/login/index.html.twig #}
{% extends 'base.html.twig' %}

{# ... #}

{% block body %}
    {% if error %}
        <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    <form action="{{ path('login') }}" method="post">
        <label for="username">Email:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}"/>

        <label for="password">Password:</label>
        <input type="password" id="password" name="_password"/>

        {# Если вы хотите контролировать URL, по которому перенаправляется пользователь при успешном входе
        <input type="hidden" name="_target_path" value="/account"/> #}

        <button type="submit">login</button>
    </form>
{% endblock %}

Caution

Змінна error, передана шаблону - екземпляр AuthenticationException. Вона може містити чутливу інформацію про помилку аутентифікації. Ніколи не використовуйте error.message: замість цього використайте властивість messageKey, як показано у цьому прикладі. Це повідомлення завжди безпечно для відображення.

Форма може виглядати як завгодно, але зазвичай вона слідує деяким угодам:

  • Елемент <form> відправляє POST маршруту login, так як ви сконфігурували це як check_path під ключем form_login у security.yaml;
  • Поле імені користувача (або будь-якого "ідентифікатора" користувача, на кшталт пошти), має ім'я _username, а поле пароля - _password.

Tip

Насправді, все це можна сконфігурувати під ключем form_login. Див. , щоб дізнатися більше.

Danger

Ця форма входу на даний момент не захищена від CSRF-атак. Прочитайте , щоб дізнатися, як захистити вашу форму входу.

Ось і все! При відправленні форми, система безпеки автоматично читає _username і параметр POST _password, завантажує користувача з постачальника користувачів, перевіряє параметри доступу користувача і або аутентифікує його, або відправляє назад у форму входу, де можна відобразити помилку.

Підсумуємо весь процес:

  1. Користувач намагається отримати доступ до захищеного ресурсу (наприклад, /admin);
  2. Брандмауер ініцією процес аутентифікації, перенаправляюючи користувача до форми входу (/login);
  3. Сторінка /login відображає форму входу за маршрутом та контролером, створеним у цьому прикладі;
  4. Користувач відправляє форму входу /login;
  5. Система безпеки (тобто аутентифікатор form_login) перехоплює запит, перевіряє параметри доступу, відправлені користувачами, аутентифікує користувача, якщо вони правильні, і відправляє користувача назад у форму входу - якщо ні.

See also

Ви можете налаштувати відповіді успішної та неуспішної спроби входу. Див. Як налаштувати відповіді аутентифікатора форми входу.

CSRF-захист у формах входу

CSRF-атак входу можна уникнути, використовуючи ту ж техніку додавання прихованих CSRF-токенів у форми входу. Компонент Security вже надає CSRF-захист, але вам треба сконфігурувати деякі опції перед її використанням.

Спочатку вам потрібно підключити CSRF у формі входу:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...

    firewalls:
        secured_area:
            # ...
            form_login:
                # ...
                enable_csrf: true

Потім, використайте функцію csrf_token() у шаблоні Twig, щоб згенерувати CSRF-токен та зберегти його в якості прихованого поля форми. За замовчуванням, HTML-поле має називатися _csrf_token, а рядок, використовуваний для генерування значення, має бути authenticate:

1
2
3
4
5
6
7
8
9
10
{# templates/security/login.html.twig #}

{# ... #}
<form action="{{ path('login') }}" method="post">
    {# ... поля логіну #}

    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

    <button type="submit">login</button>
</form>

Після цього ви захистили вашу форму входу від CSRF-атак.

Tip

Ви можете змінити ім'я поля, встановивши csrf_parameter і змінити ID токена, встановивши csrf_token_id у вашій конфігурації. Див. , щоб дізнатися більше.

Вхід JSON

Деякі додатки надають API, захищений за допомогою токенів. Такі додатки можуть використовувати кінцеву точку, що надає ці токени, засновуючись на імені користувача (або пошті) та паролі. Аутентифікатор входу JSON допомогає вам функціонально створювати це.

Включіть аутентифікатор, використовуючи налаштування json_login:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            json_login:
                # api_login - це маршрут, який ми створимо нижче
                check_path: api_login

Note

check_path підтримує URL та імена маршрутів (але не може мати обов'язкових заповнювачів - наприклад, /login/{foo}, де foo не має значення за замовчуванням).

Аутентифікатор запускається, коли клієнт запитує check_path. Спочатку створіть контролер для цього шляху:

1
2
3
$ php bin/console make:controller --no-template ApiLogin

 created: src/Controller/ApiLoginController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/ApiLoginController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ApiLoginController extends AbstractController
{
    #[Route('/api/login', name: 'api_login')]
    public function index(): Response
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/ApiLoginController.php',
        ]);
    }
}

Цей контролер входу буде викликаний після того, як аутентифікатор успішно аутентифікую користувача. Ви можете отримати аутентифікованого користувача, згенерувати токен (або те, що вам потрібно повернути) і повернути JSON-відповідь:

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
// ...
+ use App\Entity\User;
+ use Symfony\Component\Security\Http\Attribute\CurrentUser;

  class ApiLoginController extends AbstractController
  {
-     #[Route('/api/login', name: 'api_login')]
+     #[Route('/api/login', name: 'api_login', methods: ['POST'])]
-     public function index(): Response
+     public function index(#[CurrentUser] ?User $user): Response
      {
+         if (null === $user) {
+             return $this->json([
+                 'message' => 'missing credentials',
+             ], Response::HTTP_UNAUTHORIZED);
+         }
+
+         $token = ...; // как-то создать API-токен для $user
+
          return $this->json([
-             'message' => 'Welcome to your new controller!',
-             'path' => 'src/Controller/ApiLoginController.php',
+             'user'  => $user->getUserIdentifier(),
+             'token' => $token,
          ]);
      }
  }

Note

#[CurrentUser] може бути використаний лише в аргументах контролера для отримання аутентифікованого користувача. У сервісах ви використовуватимете getUser().

Ось і все! Підсумуємо процес:

  1. Клієнт (наприклад, фронтенд) робить запит POST із заголовком Content-Type: application/json до /api/login з username (навіть якщо ваш ідентифікатор насправді - пошта) і ключами password:

    1
    2
    3
    4
    {
        "username": "dunglas@example.com",
        "password": "MyPassword"
    }
  2. Система безпеки перехоплює запит, перевіряє відправлені права доступу користувача і аутентифікує його. Якщо права доступу некоректні, повертається JSON-відповідь HTTP 401 Неавторизовано, в інших випадках запускається ваш контролер;
  3. Ваш контролер створює коректну відповідь:

    1
    2
    3
    4
    {
        "user": "dunglas@example.com",
        "token": "45be42..."
    }

Tip

Формат JSON-запитів може бути сконфігурований під ключем json_login. Див. , щоб дізнатися більше.

Базовий HTTP

Аутентификація базового HTTP - це стандартизований фреймворк HTTP-аутентифікації. Він запитує права доступу (ім'я користувача та пароль), використовуючи діалог у браузері та аутентифікатор базового HTTP Symfony верифікує ці права.

Додайте ключ http_basic до вашого брандмауеру, щоб увімкнути аутентифікатор базового HTTP:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            http_basic:
                realm: Secured Area

Ось і все! Кожний раз, коли неаутентифікований користувач буде намагатися відвідати захищену сторінку, Symfony інформуватиме браузер, що йому потрібно почати базову HTTP-аутентифікацію (використовуючи заголовок відповіді WWW-Authenticate). Потім аутентифікатор верифікує права доступу та аутентифікує користувача.

Note

Ви не можете використати вихід з системи з базовим аутентифікатором HTTP. Навіть якщо ви вийдете з Symfony, ваш браузер "пам'ятає" ваші права доступу і буде відправляти їх за кожним запитом.

Вхід за посиланням

Вхід за посиланням - це безпарольний механізм аутентифікації. Користувач отримуватиме посилання з коротким строком життя (наприклад, по пошті), як аутентифікує їх на веб-сайті.

Ви можете дізнатися все про цей аутентифікатор у Як використовувати безпарольну аутентифікацію посилання входу у систему.

Токени доступу

Токени доступу часто використовуться у контекстах API. Користувач отримує токен з сервера авторизації, який його аутентифікує.

Ви можете дізнатися все про цей аутентифікатор в Як використовувати аутентифікацію токена доступу.

Сертифікати клієнтів X.509

При використанні сертифікатів клієнтів, ваш веб-сервер робить всю аутентифікацію сам. Аутентифікатор X.509, наданий Symfony, отримує пошту з "унікального імені" (DN) сертифіката клієнта. Потім він використовує цю пошту в якості ідентифікатора користувача у постачальнику користувачів.

Спочатку сконфігуруйте ваш веб-сервер, щоб підключити верифікацію сертифікатів клієнтів, і показати DN сертифікатів додатку Symfony:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
    # ...

    ssl_client_certificate /path/to/my-custom-CA.pem;

    # увімкнути верифікацію сертифікатів клієнтів
    ssl_verify_client optional;
    ssl_verify_depth 1;

    location / {
        # передати додатку DN як "SSL_CLIENT_S_DN"
        fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;

        # ...
    }
}

Потім, включіть аутентифікатор X.509, використовуючи x509 у вашому брандмауері:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            x509:
                provider: your_user_provider

За замовчуванням, Symfony отримує адресу пошти з DN двома способами:

  1. Спочатку, вона випробовує параметр сервера SSL_CLIENT_S_DN_Email, який розкрито з допомогою Apache;
  2. Якщо він не встановлений (наприклад, при використанні Nginx), вона використовує SSL_CLIENT_S_DN і співставляє значення наступного emailAddress=.

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

Віддалені користувачі

Окрім аутентифікації сертифікатів клієнта, існує багато інших модулів веб-сервера, який попередньо аутентифікує користувача (наприклад, kerberos). Видалений аутентифікатор користувачів надає базову інтеграцію для таких сервісів.

Такі модулі часто розкривають аутентифікованого користувача у змінній середовища REMOTE_USER. Аутентифікатор видаленого користувача використовує це значення в якості ідентифікатора користувача, щоб завантажити відповідного користувача.

Включіть аутентифікацію видаленого користувача, використовуючи ключ remote_user:

1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    firewalls:
        main:
            # ...
            remote_user:
                provider: your_user_provider

Tip

Ви можете налаштувати ім'я цієї змінної сервера під ключем remote_user. Див. довідник конфігурації , щоб дізнатися більше.

Обмеження спроб входу

Symfony надає базовий захист від брутальних атак на вхід в систему завдяки
компоненту Rate Limiter. Якщо ви ще не використовували цей

компонент у вашому додатку, встановіть його перед використанням цієї функції:

1
$ composer require symfony/rate-limiter

Потім увімкніть цю функцію за допомогою налаштування login_throttling:

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.yaml
security:
    # ви маєте використати менеджер аутентифікатора
    enable_authenticator_manager: true

    firewalls:
        # ...

        main:
            # ...

            # за замовчуванням, функція дозволяє 5 спроб входу за хвилину
            login_throttling: null

            # сконфігурувати максимум спроб входу (за хвилину)
            login_throttling:
                max_attempts: 3

            # сконфігурувати максимум спроб входу за заданий період часу
            login_throttling:
                max_attempts: 3
                interval: '15 minutes'

            # використати користувацький обмежувач швидкості через його ID сервіса
            login_throttling:
                limiter: app.my_login_rate_limiter

Note

Значення опції interval має бути числом, за яким слідує будь-яка одиниця, прийнята відносними PHP-форматами дат (наприклад, 3 секунди, 10 годин, 1 день тощо)

Внутрішньо, Symfony використовує компонент Rate Limiter, який за замовчуванням викоритсовує кеш Symfony, щоб зберігати попередні спроби входу у систему. Однак, ви можете реалізувати користувацьке сховище .

За замовчуванням, спроби входу обмежені max_attempts (за замовчуванням: 5) невдалими запитами за IP address + username і 5 * max_attempts невдалими запитами за IP address. Друге обмеження захищає від того, щоб хакер не використовува багато імен користувачів, обходячи перше обмеження, не порушуючи роботу нормальних користувачів у великих мережах (на кшталт офісів).

Tip

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

Якщо вам потрібний складніший алгоритм обмежень, створіть клас, що реалізує RequestRateLimiterInterface (або використайте DefaultLoginRateLimiter) і встановіть опцію limiter в її ID сервісу:

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.yaml
framework:
    rate_limiter:
        # визначте 2 обмежувача (один для username+IP, другий - для IP)
        username_ip_login:
            policy: token_bucket
            limit: 5
            rate: { interval: '5 minutes' }

        ip_login:
            policy: sliding_window
            limit: 50
            interval: '15 minutes'

services:
    # наш користувацький обмежувач
    app.login_rate_limiter:
        class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
        arguments:
            # globalFactory - обмежувач для IP
            $globalFactory: '@limiter.ip_login'
            # localFactory - обмежувач для username+IP
            $localFactory: '@limiter.username_ip_login'

security:
    firewalls:
        main:
            # використати користувацький обмежувач за його ID сервісу
            login_throttling:
                limiter: app.login_rate_limiter

Налаштуйте успішну та невдалу поведінку аутентифікації

Якщо ви хочете налаштувати обробку успішної або невдалої аутентифікації, вам не потрібно глобально перезаписувати відповідних слухачів. Замість цього ви можете встановити власні обробники успішної та невдалої аутентифікації, реалізувавши AuthenticationSuccessHandlerInterface або AuthenticationFailureHandlerInterface.

Прочитайте як налаштувати обробник успіху для отримання додаткової інформації про це.

Програмний вхід у систему

Ви можете впустити користувача у систему програмно, використовуючи метод `login()` помічника Security:

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
// src/Controller/SecurityController.php
namespace App\Controller\SecurityController;

use App\Security\Authenticator\ExampleAuthenticator;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;

class SecurityController
{
    public function someAction(Security $security): Response
    {
        // виконати аутентифікацію користувача
        $user = ...;

        // впустити користувача у систему у поточному брандмауері
        $this->security->login($user);

        // якщо брандмауер має більше одного аутентифікатора, ви маєте передати його чітко,
        // використовуючи імʼя вбудованих аутентифікаторів...
        $this->security->login($user, 'form_login');
        // ...або id сервісу користувацьких аутентифікаторів
        $this->security->login($user, ExampleAuthenticator::class);

        // ви також можете увійти в систему в іншому бранмауері...
        $this->security->login($user, 'form_login', 'other_firewall');

        // ...і додати бейджі
        $security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()]);

        // використати логіку перенаправлення, що застосовується до звичайного входу
        $redirectResponse = $security->login($user);
        return $redirectResponse;

        // або використати користувацьку логіку перенаправлення (наприклад, перенаправляти користувачів на сторінку їхнього акаунта)
        // return new RedirectResponse('...');
    }
}

Вихід з системи

Щоб увімкнути вихід з системи, активуйте параметр конфігурації logout під вашим брандмауером:

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            logout:
                path: app_logout

                # куди перенаправляти після виходу
                # target: app_any_route

Після цього Symfony скасує аутентифікацію користувачів, які переходять на сконфігурований path, і перенаправлятиме їх до сконфігурованої target.

Якщо вам потрібно вказати шлях до виходу з системи, ви можете використати імʼя маршруту _logout_<firewallname> (наприклад, _logout_main).

Якщо ваш проект не використовує Symfony Flex , переконайтеся, що ви імпортували завантажувач маршруту виходу з системи у ваші маршрути:

1
2
3
4
# config/routes/security.yaml
_symfony_logout:
    resource: security.route_loader.logout
    type: service

Програмний вихід з системи

Ви можете вивести користувача з системи програмно, використовуючи метод logout()
помічника Security:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/SecurityController.php
namespace App\Controller\SecurityController;

use Symfony\Bundle\SecurityBundle\Security;

class SecurityController
{
    public function someAction(Security $security): Response
    {
        // вивести користувача з системи у поточному брандмауері
        $response = $security->logout();

        // ви також можете відключити вихід з системи csrf
        $response = $security->logout(false);

        // ... повернути $response (якщо встановлено) або, наприклад, перенаправити на домашню сторінку
    }
}

Користувача буде виведено з системи у брандмауері запиту. Якщо запит не знаходиться за брандмауером, буде викликано \LogicException.

Налаштування виходу

// src/EventListener/LogoutSubscriber.php namespace AppEventListener;

use SymfonyComponentEventDispatcherEventSubscriberInterface; use SymfonyComponentHttpFoundationRedirectResponse; use SymfonyComponentRoutingGeneratorUrlGeneratorInterface; use SymfonyComponentSecurityHttpEventLogoutEvent;

class LogoutSubscriber implements EventSubscriberInterface { public function __construct( private UrlGeneratorInterface $urlGenerator ) { }

public static function getSubscribedEvents(): array { return [LogoutEvent::class => 'onLogout']; }

public function onLogout(LogoutEvent $event): void { // отримати токен безпеки сесії, яка ось-ось буде виведена з системи $token = $event->getToken();

// отримати поточний запит $request = $event->getRequest();

// отримати поточну відоповідь, якщо вона вже встановлена іншим слухачем $response = $event->getResponse();

// сконфігурувати користувацьку відповідь виходу з системи на домашню сторінку $response = new RedirectResponse( $this->urlGenerator->generate('homepage'), RedirectResponse::HTTP_SEE_OTHER ); $event->setResponse($response);

}

}

Налаштування шляху виходу з системи

Інший варіант - вказати path як назву маршруту. Це може бути корисно якщо ви хочете, щоб URI виходу були динамічними (наприклад, перекладалися відповідно до поточної локалі). У такому випадку вам доведеться створити цей маршрут самостійно:

1
2
3
4
5
6
# config/routes.yaml
app_logout:
    path:
        en: /logout
        fr: /deconnexion
    methods: GET

Потім передайте ім'я маршруту до опції path:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            logout:
                path: app_logout

Отримання об'єкта користувача

Після аутентифікації, об'єкт User поточного користувача доступний через ярлик getUser() у базовому контролері :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProfileController extends AbstractController
{
    public function index(): Response
    {
        // зазвичай ви захочете спочатку переконатися, що користувач аутентифікований
        // див. "Authorization" нижче
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

        // повертає ваш об'єкт User або null, якщо користувач не аутентифікований
        // використати вбудовану документацію, щоб повідомити редактору ваш точний клас User
        /** @var \App\Entity\User $user */
        $user = $this->getUser();

        // Викликати ті методи, які ви додали у ваш клас User
        // Наприклад, якщо ви додали метод getFirstName(), ви можете використати його.
        return new Response('Well hi there '.$user->getFirstName());
    }
}

Отримання користувача з сервісу

Якщо вам потрібно отримати користувача, виконавшого вхід, з сервісу, використайте сервіс Security:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Service/ExampleService.php
// ...

use Symfony\Bundle\SecurityBundle\Security;

class ExampleService
{
        // Уникайте виклику getUser() у конструкторі: авторизація може бути ще не
        // виконана. Замість цього, збережіть весь об'єкт Security.
    public function __construct(
        private Security $security,
    ){
    }

    public function someMethod(): void
    {
        // повертає об'єкт User або null, якщо він не аутентифікований
        $user = $this->security->getUser();

        // ...
    }
}

Отримання користувача у шаблоні

У шаблоні Twig об'єкт користувача доступний через змінну app.user завдяки глобальній змінній додатку Twig :

1
2
3
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <p>Email: {{ app.user.email }}</p>
{% endif %}

Контроль доступу (Авторизація)

Тепер користувачі можуть виконувати вхід у ваш застосунок, використовуючи форму входу. Чудово! Далі, вам треба дізнатися, як відмовляти у доступі та працювати з об'єктом Користувача. Це називається авторизація, і її робота - вирішити, чи може користувач отримати доступ до якогось джерела (URL, об'єкта model, методу виклику, ...).

Процес авторизації має дві сторони:

  1. Користувач отримує конкретну роль при виконанні входу (наприклад, ROLE_ADMIN).
  2. Ви додаєте код, щоб джерело (наприклад, URL, контролер) вимагав конкретний "атрибут" (наприклад, роль на кшталт ROLE_ADMIN), перед тим, як стати доступним.

Ролі

Коли користувач виконує вхід, Symfony викликає метод getRoles() у вашому об'єкті User, щоб визначити, які ролі має користувач. У класі User, який було згенеровано раніше, ролі - це масив, що зберігається у базі даних, і кожний користувач завжди має хоча б одну роль: ROLE_USER:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Entity/User.php

// ...
class User
{
    #[ORM\Column(type: 'json')]
    private array $roles = [];

    // ...
    public function getRoles(): array
    {
        $roles = $this->roles;
        // гарантувати, що кожний користувач має хоча б ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }
}

Це гарне значення за замовчуванням, але ви можете робити що хочете, щоб визначити, які ролі повинен мати користувач. Єдиним правило є те, що кожна роль повинна починатися з префіксу ROLE_ - інакше все не працюватиме, як очікується. Окрім цього, роль - це просто рядок, і ви можете винаходити все, що вам потрібно (наприклад, ROLE_PRODUCT_ADMIN).

Ви будете використовувати ролі далі, щоб надавати доступ до конкретних розділів вашого сайту.

Ієрархічні ролі

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

1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    # ...

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Користувачі з роллю ROLE_ADMIN також матимуть роль ROLE_USER. Користувачі з ROLE_SUPER_ADMIN, автоматично матимуть ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH і ROLE_USER (що наслідуються з ROLE_ADMIN).

Caution

Для того, щоб ієрархія ролей працювала, не використовуйте $user->getRoles() вручну. Наприклад, у контролері, що розширюється з базового контролера :

1
2
3
4
5
6
// ПОГАНО - $user->getRoles() не знатиме про ієрархію ролей
$hasAccess = in_array('ROLE_ADMIN', $user->getRoles());

// ДОБРЕ - використання нормальних методів безпеки
$hasAccess = $this->isGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('ROLE_ADMIN');

Note

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

Додайте код для відмови у доступі

Існує два способи відмовити у доступі до чогось:

  1. access_control у security.yaml дозволяє вам захищати патерни URL (наприклад, /admin/*). Простіше, але менш гнучко;
  2. у вашому контролері (або іншому коді) .

Захист паттернів URL (access_control) ...........ю.........................

Найбазовійши спосіб захистити частину вашого додатку - захистити весь патерн URL у security.yaml. Наприклад, вимагати ROLE_ADMIN для всіх URL, які починаються з /admin, ви можете:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# config/packages/security.yaml
security:
    # ...

    firewalls:
        # ...
        main:
            # ...

    access_control:
        # вимагати ROLE_ADMIN для /admin*
        - { path: '^/admin', roles: ROLE_ADMIN }

        # або вимагати ROLE_ADMIN або IS_AUTHENTICATED_FULLY для /admin*
        - { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }

        # значення 'path' може бути будь-яким валідним регулярним виразом
        # (это будет совпадать с URL вроде /api/post/7298 и /api/comment/528491)
        - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }

Ви можете визначити стільки патернів URL, скільки вам потрібно - кожний буде регулярним виразом. АЛЕ лише один буде співставлений з кожним запитом: Symfony починає зверху списку і зупиняється, коли знаходить перше співпадіння:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...

    access_control:
        # співставляє з /admin/users/*
        - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }

        # співставляє з /admin/* окрім всього іншого, що співпадає з правилом вище
        - { path: '^/admin', roles: ROLE_ADMIN }

Додавання на початку шляху ^, означає, що тільки URL, які починаються з патерну, будуть співставлятися. Наприклад, шлях /admin (без ^) співпадає з /admin/foo, але крім цього і з URL на кшталт /foo/admin.

Кожний access_control може також співставлятися з IP-адресою, іменем хостингу і HTTP-методами. Він також може бути використаний для пренаправлення користувача на https версію патерна URL.

Див. Як працює безпека access_control?.

Безпека контролерів та іншого коду

Ви можете відмовити у доступі зсередини контролера:

1
2
3
4
5
6
7
8
9
10
// src/Controller/AdminController.php
// ...

public function adminDashboard(): Response
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    // або додати необов'язкове повідомлення, яке бачать розробники
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
}

Ось і все! Якщо у доступі відмовлено, викликається спеціальний AccessDeniedException, і ніякий код у вашому контролері більше не викликається. Потім, відбувається одне з двох:

  1. Якщо користувач ще не увійшов у систему, його попросять увійти (наприклад, перенаправлять на сторінку входу).
  2. Якщо користувач вже у системі, але не має ролі ROLE_ADMIN, йому відобразиться помилка доступу 403 (яку ви можете налаштувати ).

Іншим способом убезпечити одну або більше дій контролера є використання атрибуту. У наступному прикладі, усі дії контролера вимагатимуть дозволу ROLE_ADMIN, окрім adminDashboard(), який вимагатиме дозволу ROLE_SUPER_ADMIN:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Controller/AdminController.php
// ...

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
    // Опціонально ви можете встановити користувацьке повідомлення, яке буде відображено користувачу
    #[IsGranted('ROLE_SUPER_ADMIN', message: 'You are not allowed to access the admin dashboard.')]
    public function adminDashboard(): Response
    {
        // ...
    }
}

Якщо ви хочете використовувати користувацький статус-код замість стандартного (який є 403), це можна зробити, встановивши за допомогою аргументу statusCode:

1
2
3
4
5
6
7
8
9
10
// src/Controller/AdminController.php
// ...

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN', statusCode: 423)]
class AdminController extends AbstractController
{
    // ...
}

Ви також можете встановити внутрішній код виключення AccessDeniedException, який генерується з аргументом exceptionCode:

1
2
3
4
5
6
7
8
9
10
// src/Controller/AdminController.php
// ...

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN', statusCode: 403, exceptionCode: 10010)]
class AdminController extends AbstractController
{
    // ...
}

Контроль доступу у шаблонах

Якщо ви хочете перевірити, чи має поточний користувач певну роль, ви можете використати вбудовану хелпер-функцію is_granted() у будь-якому шаблоні Twig:

1
2
3
{% if is_granted('ROLE_ADMIN') %}
    <a href="...">Delete</a>
{% endif %}

Безпека інших сервісів

Ви можете перевірити доступ у будь-якому місці вашого коду, впровадивши сервіс Security. Наприклад, припустимо, що у вас є сервіс SalesReportManager, і ви хочете додати додаткові деталі лише для користувачів, що мають роль ROLE_SALES_ADMIN:

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
// src/SalesReport/SalesReportManager.php

  // ...
  use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+ use Symfony\Bundle\SecurityBundle\Security;

  class SalesReportManager
  {
+     public function __construct(
+         Security $security,
+     ) {
+     }

      public function generateReport(): void
      {
          $salesData = [];

+         if ($this->security->isGranted('ROLE_SALES_ADMIN')) {
+             $salesData['top_secret_numbers'] = rand();
+         }

          // ...
      }

      // ...
  }

Якщо ви використовуєте конфігурацію services.yaml за замовчуванням , Symfony автоматично передасть security.helper вашому сервісу, завдяки автомонтуванню та підказці Security.

Ви також можете мати сервіс нижнього рівня AuthorizationCheckerInterface. Він робить те ж, що і Security, але дозволяє вам додавати підказу більш конкретного інтерфейса.

Дозвіл незахищеного доступу (т.з. анонімні користувачі)

Коли відвідувач ще не увійшов на ваш сайт, він розглядається як "неаутентифікований" і не має ніяких ролей. Це заблокує йому доступ до ваших сторінок, якщо ви визначили правило access_control.

У конфігурації access_control ви можете використати атрибут безпеки PUBLIC_ACCESS, щоб виключити деякі маршрути для неаутентифікованого доступу (наприклад, сторінку входу):

1
2
3
4
5
6
7
8
9
10
11
# config/packages/security.yaml
security:
    enable_authenticator_manager: true

    # ...
    access_control:
        # дозволити неаутентифікованим користувачам доступ до форми входу
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }

        # але вимагати аутентифікацію для всіх інших адмінських маршрутів
        - { path: ^/admin, roles: ROLE_ADMIN }

Дозвіл доступу анонімним користувача у користувацькому виборці

Якщо ви використовуєте користувацького виборця, ви можете дозволити анонімним користувачам доступ, перевіривши, чи встановлено у токені користувача:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Security/PostVoter.php
namespace App\Security;

// ...
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // ...

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
    {
        // ...

        if (!$token->getUser() instanceof UserInterface) {
            // користувач не аутентифікований, наприклад, дозволити йому бачити
            // лише публічні пости
            return $subject->isPublic();
        }
    }
}

Установка індивідуальних дозволів користувачів

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

Перевірка, чи виконав користувач вхід

Якщо ви хочете тільки перевірити, чи знаходиться користувач у системі (вам все одно, які у нього ролі), у вас є два наступних варіанти.

По-перше, якщо ви дали кожному користувачу ROLE_USER, ви можете перевірити цю роль.

По-друге, ви можете використати спеціальний "атрибут" на місці ролі:

1
2
3
4
5
6
7
8
// ...

public function adminDashboard(): Response
{
    $this->denyAccessUnlessGranted('IS_AUTHENTICATED');

    // ...
}

Ви можете використати IS_AUTHENTICATED_FULLY всюди, де використовуються ролі: на кшталт access_control або в Twig.

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

  • IS_AUTHENTICATED_FULLY: Схоже на IS_AUTHENTICATED_REMEMBERED, але потужніше. Користувачі, які у системі лише через "кукі запам'ятати мене", матимуть IS_AUTHENTICATED_REMEMBERED, але не матимуть IS_AUTHENTICATED_FULLY.
  • IS_REMEMBERED: Лише користувачі, які аутентифіковані з використанням функціоналу запам'ятати мене, (тобто кукі запам'ятати мене).
  • IS_IMPERSONATOR: Коли поточний користувач імперсонує іншого користувача у цій сесії, атрибут співпадатиме.

Розуміння оновлення користувачів з сесії

У кінці кожного запиту (окрім випадків, коли ваш брандмауер stateless), ваш об'єкт User серіалізується у сесію. На початку наступного запиту, він десеріалізується, а потім передається вашому постачальнику користувачів для "оновлення" (наприклад, запитів Doctrine для свіжого користувача).

Потім, два об'єкта User (початковий з сесії та оновлений об'єкт User) "порівнюються", щоб побачити, чи "рівні" вони. За замовчуванням, базовий клас AbstractToken порівнює зворотні значення методів getPassword(), getSalt() і getUserIdentifier(). Якщо якісь з них відрізняється, ваш користувач вийде з системи. Це міра безпеки, щоб гарантувати, що зловмисні користувачі будуть деаутентифіковані, якщо зміняться базові дані користувача.

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

У такому випадку, перегляньте логіку серіалізації (наприклад, методи __serialize() або serialize()) у вашому класі користувача (якщо вона є), щоб переконатися, що всі необхідні поля серіалізуються, а також виключити усі поля, які не треба серіалізувати (наприклад, відношення Doctrine).

Порівняння користувачів вручну з EquatableInterface

Або, якщо вам потрібно більше контролю над процесом "порівняння користувачів", зробіть так, щоб ваш клас User реалізовував EquatableInterface. Після цього, ваш метод isEqualTo() викликатиметься при порівнянні користувачів, замість базової логіки.

Події безпеки

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

Tip

Кожний брандмауер Безпеки має власний диспетчер подій (security.event_dispatcher.FIREWALLNAME). Події оголошуються як у глобальному, так і у диспетчері брандмауера. Ви можете зареєструватися у диспетчері брандмауера, якщо ви хочете, щоб ваш слухач викликалвся лише для конкретного брандмауера. Наприклад, якщо у вас є брандмауери api і main, використайте цю конфігурацію, щоб реєструвати лише подію виходу з системи у брандмауері main:

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    App\EventListener\LogoutSubscriber:
        tags:
            - name: kernel.event_subscriber
              dispatcher: security.event_dispatcher.main

Події аутентифікації

CheckPassportEvent
Оголошується після того, як аутентифікатор створив паспорт безпеки . Слухачі цієї події проводять реальні перевірик аутентифікації (на кшталт перевірки паспорту, валідації CSRF-токена і т.д.)
AuthenticationTokenCreatedEvent
Оголошується після валідації паспорту і того, як аутентифікатор створив токен безпеки (і користувача). Це може бути використано у просунутих випадках застосування, де вам потрібно змінювати створений токен (наприклад, для мульти-факторної аутентиафікації).
AuthenticationSuccessEvent
Оголошується, коли аутентифікація наближається до успіху. Це остання подія, яка може призвести до невдалої аутентифікації, викликавши AuthenticationException.
LoginSuccessEvent
Оголошується після того, як аутентифікація була повністю успішна. Слухачі цієї події можуть змінювати відповідь, відправлену користувачу.
LoginFailureEvent
Оголошується після виклику AuthenticationException під час аутентифікації. Слухачі цієї події змінюють відповідь помилки та відправляють його назад користувачу.

Інші події

InteractiveLoginEvent
Оголошується після того, аутентифікація була повністю успішною тільки тоді, коли аутентифікатор реалізує InteractiveAuthenticatorInterface, що вказує на те, що вхід до системи вимагає явних дій користувача (наприклад, заповнення форми входу). Слухачі цієї події можуть змінювати відповідь, що надсилається користувачеві.
LogoutEvent
Оголошується прямо перед тим, як користувач виходить з вашого додатку. Див. .
TokenDeauthenticatedEvent
Оголошується, коли користувач деаутентифікований, наприклад, через зміну пароля. Див. Постачальники користувачів Security.
SwitchUserEvent
Оголошується після завершення "імперсонації іншого". Див. Як імперсонувати користувача.

Часто поставлені питання

У мене може бути багато брандмауерів?
Так! Але зазвичай це не потрібно. Кожний брандмауер - це як окрема система безпеки, аутентифікація в одному не робить вас аутентифікованим у іншому. Один брандмауер може мати багато способів дозволу аутентифікації (наприклад, форму входу, аутентифікація ключа API і LDAP).
Безпека, схоже, не працює на моіх сторінках помилок
Так як маршрутизація проводиться до безпеки, сторінки помилок 404 не охоплюються жодним брандмауером. Це означає, що ви не можете перевіряти безпеку або навіть отримати доступ до об'єкта користувача на таких сторінках. Див. Як налаштувати сторінки помилок, щоб дізнатися більше.
Схоже, моя аутентифікація не працює: помилок немає, але я не можу увійти
Іноді аутентифікація може бути успішною, але піся перенаправлення ви одразу ж виходите з системи, через проблеми із завантаженням User з сесії. Щоб побачити, чи в цьому проблема, перевірте ваш файл логів (var/log/dev.log) на предмет повідомлень логів.
Не можу оновити токен, так як користувач змінився
Якщо ви бачите це, є два варіанти, чому це так. По-перше, може бути проблема з завантаженням вашого Користувача з сесії. Див. Постачальники користувачів Security. По-друге, якщо визначена інформація користувача змінилася у базі даних з моменту останнього оновлення сторінки, Symfony спеціально виведе користувача з системи з міркувань безпеки.