Безопасность

Дата обновления перевода 2024-08-01

Безопасность

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
// 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;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials(): void
    {
        // Если вы храните любые временные чувствительные данные пользователя, очистите их здесь
        // $this->plainPassword = null;
    }
}

Tip

Начиная с MakerBundle: v1.57.0 - Вы можете передавать --with-uuid или --with-ulid в make:user. Используя Компонент Uid от Symfony, создается сущность User с типом id в виде :ref:a05daa4c14c5bacd568cb31237544450398719d3int``.

Если ваш пользователь является сущностью 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). Поставщик "обновляет" пользователя (например, снова делает запрос к БД для свежих данных), чтобы убедиться, что вся информация пользователя обновлена (и, если необходимо, пользователь деаутентифицируется/выводится из системы, если что-то изменилось). См. Поставщики пользователей Безопасности, чтобы узнать больше об этом процессе.

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 строку с хешированным паролем для этого пользователя
     */
    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.

Tip

Команда-мейкер 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
17
18
19
20
# config/packages/security.yaml
security:
    # ...
    firewalls:
        # порядок, в котором определены брандмауэры, очень важен, так как
        # запрос будет обработан первым брандмауэром, чей паттерн совпадет
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        # брандмауэр без паттерна должен быть определен последним, потому что он будет совпадать со всеми запросами
        main:
            lazy: true
            # поставщик, который вы установили ранее внутри поставщиков
            provider: app_user_provider

            # активировать различные способы аутентификации
            # 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 означает, что совпадают dct URL). Брандмауэр может иметь множество режимов аутентификации, другими словами - множество способов задать вопрос "Ты кто?".

Зачастую пользовател неизвестен (т.е. не выполнил вход в систему), когда он впервые попадает на ваш сайт. Если вы посетите свою домашнюю страницу прямо сейчас, у вас будет доступ, и вы увидите, что вы посещаете страницу за брандмауэром в панели инструментов:

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

Посещение 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
// src/Service/ExampleService.php
// ...

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

class ExampleService
{
    public function __construct(
        // Избегайте вызова getFirewallConfig() в конструкторе: авторизация может быть еще
        // не завершена. Вместо этого, сохраните весь объект Security.
        $this->security = $security;
    }

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

        // ...
    }
}

Аутентификация пользователей

Во время аутентификации, система пытается найти соответствующего пользователя для посетителя страницы. Традиционно, это делается с помощью формы входа или базового HTTP-диалога в браузере. Однако, SecurityBundle поставляется со множеством других аутентификаторов:

Tip

Если ваше приложение пропускает пользователей в систему с помощью сторонних сервисов, вроде Google, Facebook или Twitter (социальный вход), посмотрите на общественный пакет HWIOAuthBundle.

Форма входа

Большинство сайтов имеют форму входа, где пользователи проходят аутентификацию, используя идентификатор (например, адрес почты или имя пользователя) и пароль. Этот функционал предоставлен встроенным FormLoginAuthenticator.

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

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',
        ]);
    }
}

Затем, подключите FormLoginAuthenticator, используя настройку form_login:

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

    firewalls:
        main:
            # ...
            form_login:
                # "app_login" - это имя ранее созданного маршрута
                login_path: app_login
                check_path: app_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. Система безопасности (т.e. аутентификатор form_login) перехватывает запрос, проверяет параметры доступа, отправленные пользователем, аутентифицирует пользователя, если они правильные, и отправляет пользователя обратно в форму входа - если нет.

See also

Вы можете настроить ответы успешной и неуспешной попытки входа. См. Как настроить ответы аутентификатора формы входа.

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

CSRF-атаки входа можно предотвратить, используя ту же технику добавления спрятанных CSRF-токенов в формы входа. Компонент Безопасность уже предоставляет 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">
    {# ... the login fields #}

    <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. См. the configuration reference , чтобы узнать больше.

Удаленные пользователи

Кроме аутентификации сертификатов клиента, существует много других модулей веб-сервера, который предварительно аутентифицируют пользователя (например, 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
# config/packages/security.yaml
security:

    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 seconds, 10 hours, 1 day и т.д.)

Внутренне, 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.

Tip

Если вам нужно сослаться на путь выхода из системы, вы можете использовать имя маршрута _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.

Настройка выхода

В некоторых случаях вам нужно будет выполнить дополнительную логику после выхода из системы (например, инвалидировать некоторые токены), или вам может захотеться настроить то, что происходит после выхода. Во время выхода из системы, запускается LogoutEvent. Зарегистрируйте слушателя или подписчика событий, чтобы выполнять пользовательскую логику:

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

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;

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:

// src/Controller/AdminController.php // ...

use SymfonyComponentSecurityHttpAttributeIsGranted;

#[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();
        }
    }
}

Установка индивидуальных разрешений пользователей

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

Проверка, выполнил ли пользователь вход (IS_AUTHENTICATED_FULLY)

Если вы хотите проверить только выполнил ли пользователь вход (но вам не важны роли), у вас есть два варианта.

Первый, если вы дали каждому пользователю 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_REMEMBERED: Все пользователи, выполнившие вход, имеют его, даже если они в системе из-за "куки запомнить меня". Даже если вы не используете функционал запомнить меня, вы можете использовать это, чтобы проверять, выполнил ли пользователь вход.
  • 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()) в вашем классе пользователя (если она есть), чтобы убедиться, что все необходимые поля сериализуются.

Сравнение пользователей вручную с 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
Запускается когда пользователь деаутентифицирован, например, из-за изменения пароля. См. Поставщики пользователей Безопасности.
SwitchUserEvent
Запускается после завершения "имитации другого". См. Как имперсонализировать пользователя.

Часто задаваемые вопросы

У меня может быть много брандмауэров?
Да! Но обычно это не нужно. Каждый брандмауэр - это как отдельная система безопасности, аутентификации в одном не делает вас аутентифицированным в другом. Один брандмауэр может иметь множество способов разрешения аутентификации (например, форму входа, аутентификацию ключа API и LDAP).
Безопасность, похоже, не работает на моих страницах ошибок
Так как маршрутизация проводится до безопасности, страницы ошибок 404 не охватываются ни одним брандмауэром. Это означает, что вы не можете проверять безопасность или даже получить доступ к объекту пользователя на таких страницах. См. Как настроить страницы ошибок, чтобы узнать больше.
Похоже, моя аутентификация не работает: ошибок нет, но я не могу войти
Иногда аутентификация может быть успешной, но после перенаправления вы сразу же выходите из системы, из-за проблемы с загрузкой User из сессии. Чтобы увидеть, в этом ли проблема, проверьте ваш файл логов (var/log/dev.log) на предмет сообщения логов:
Не могу обновить токен, так как пользователь изменился
Если вы видите это, есть два варианта, почему это так. Во-первых, может быть проблема с загрузкой вашего Пользователя из сессии. См. Поставщики пользователей Безопасности. Во-вторых, если определенная информация пользователя изменилась в БД с момента последнего обновления страницы, Symfony специально выведет пользователя из системы из соображений безопасности.

Узнайте больше