Дата обновления перевода 2021-12-25

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

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.

Caution

Безопасность Symfony получила существенные изменения в 5.3. Эта статья разъясняет новую систему, основанную на аутентификаторе (идентифицированную опцией конфигурации enable_authenticator_manager: true).

Смотрите версию этой документации 5.2, если вы все еще используете предыдущую систему безопасности.

Пользователь

Разрешения в 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]:
 > Пользователь

 Вы хотите хранить данные пользователя в БД (через 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 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 $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $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;
    }

    /**
     * @deprecated since Symfony 5.3
     */
    public function getUsername(): 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;
    }
}

New in version 5.3: Интерфейс PasswordAuthenticatedUserInterface и метод getUserIdentifier() были представлены в Symfony 5.3.

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

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

Загрузка пользователя: Поставщик пользователей

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

  • YAML
    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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <provider name="app_user_provider">
                <entity class="App\Entity\User" property="email"/>
            </provider>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/packages/security.php
    use App\Entity\User;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $security->provider('app_user_provider')
            ->entity()
                ->class(User::class)
                ->property('email')
        ;
    };
    

Этот поставщик пользователей знает, как (пере)загружать пользователей из хранилища (например, БД), основываясь на “идентификаторе пользователя” (например, адресе почты или имени пользователя). Конфигурация выше использует Doctrine для загрузки сущности User, используя свойство email как “user identifier”.

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

Загрузка пользователя, основываясь на идентификаторе
Во время входа (или любой другой аутентификации), поставщик загружает пользователя, основываясь на его идентификаторе пользователя. Некоторые другие функции, вроде имитации пользователя и Запомнить меня также используют это.
Перезагрузка пользователя из сессии
В начале каждого запроса, пользователь загружается из сессии (кроме случаев, когда ваш брандмауэр stateless). Поставщик “обновляет” пользователя (например, снова делает запрос к БД для свежих данных), чтобы убедиться, что вся информация пользователя обновлена (и, если необходимо, пользователь деаутентифицируется/выводится из системы, если что-то изменилось). См. Understanding how Users are Refreshed from the Session, чтобы узнать больше об этом процессе.

Symfony поставляется с несколькими встроенными поставщиками пользователей:

Поставщик пользователей сущности
Загружает пользователей из БД, используя Doctrine;
Поставщик пользователей LDAP
Загружает пользователей с LDAP-сервера;
Поставщик пользователей памяти
Загружает пользователей из файла конфигурации;
Цепной постащик пользователей
Слияет два или более поставщика пользоваталей в нового поставщика пользователей.

Встроенные поставщики пользователей покрывают наиболее распространенные задачи приложений, но вы также можете создать собственного пользовательского поставщика пользователей.

Note

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

Регистрация пользователя: Хеширование паролей

Множество приложений требуют входа в систему с помощью пароля. Для таких приложений SecurityBundle предоставляет хеширование паролей и верификацию функциональности.

Для начала, убедитесь, что ваш класс User реализует PasswordAuthenticatedUserInterface:

// 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 должен был сделать это за вас:

  • YAML
    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'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
            <!-- Использовать нативный хешировщик паролей, который автоматически выбирает и мигрирует лучший
                 возможный алгоритм хеширования (начиная с Symfony 5.3 это "bcrypt") -->
            <password-hasher class="Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" algorithm="auto"/>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/packages/security.php
    use App\Entity\User;
    use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
    
    return static function (SecurityConfig $security) {
        // ...
    
        // Использовать нативный хешировщик паролей, который автоматически выбирает и мигрирует лучший
        // возможный алгоритм хеширования (начиная с Symfony 5.3 это "bcrypt")
        $security->passwordHasher(PasswordAuthenticatedUserInterface::class)
            ->algorithm('auto')
        ;
    };
    

New in version 5.3: Опция password_hashers была представлена в Symfony 5.3. В предыдущих версиях она называлась encoders.

Теперь, когда Symfony знает как вы хотите хешировать пароли, вы можете использовать сервис UserPasswordHasherInterface, чтобы делать это до сохранения ваших пользователей в базу данных:

// src/Controller/RegistrationController.php
namespace App\Controller;

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

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

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

        // ...
    }
}

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

Прочтите больше обо всех доступных хешироващиках и миграции паролей в Password Hashing and Verification.

Брандмауэр

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

  • YAML
     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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
            <firewall name="dev"
                pattern="^/(_(profiler|wdt)|css|images|js)/"
                security="false"/>
    
            <firewall name="main"
                lazy="true"/>
    
            <!-- активировать разные способы аутентификации
                 https://symfony.com/doc/current/security.html#firewalls-authentication -->
    
            <!-- https://symfony.com/doc/current/security/impersonating_user.html -->
            <!-- <switch-user/> -->
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
        $security->firewall('dev')
            ->pattern('^/(_(profiler|wdt)|css|images|js)/')
            ->security(false)
        ;
    
        $security->firewall('main')
            ->lazy(true)
    
            // активировать разные способы аутентификации
            // https://symfony.com/doc/current/security.html#firewalls-authentication
    
            // https://symfony.com/doc/current/security/impersonating_user.html
            // ->switchUser(true)
        ;
    };
    

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

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

Все настоящие URL обрабатываются брандмауэром main (отсутствие ключа pattern означает, что совпадают dct URL). Брандмауэр может иметь множество режимов аутентификации, другими словами - множество способов задать вопрос “Ты кто?”.

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

_images/anonymous_wdt.png

Посвещение URL под другим брандмауэром не обязательно требует вашей аутентификации (например, форма входа должна быть доступна, или некоторые части вашего приложения публичны). Вы узнаете как ограничить доступ по URL, контроллерам или чему-либо еще в вашем брандмауэере в разделе контроль доступа section.

Tip

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

Note

Если вы не видите панель инструментов, установите профилировщик:

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

Теперь, когда мы понимаем наш брандмауэр, следующий шаг - создать способ аутентификации ваших пользователей!

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

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

Tip

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

Форма входа

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

Для начала, создайте контроллер для формы входа:

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\Annotation\Route;

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

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

  • YAML
     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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
            <firewall name="main">
                <!-- "login" - это имя ранее созданного маршрута -->
                <form-login login-path="login" check-path="login"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $mainFirewall = $security->firewall('main');
    
        // "login" - это имя ранее созданного маршрута
        $mainFirewall->formLogin()
            ->loginPath('login')
            ->checkPath('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: '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. См. form_login Authentication, чтобы узнать больше.

Caution

Эта форма входа на данный момент не защищена от CSRF-атак. Прочтите CSRF Protection in Login Forms, чтобы узнать, как защитить вашу форму входа.

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

Подведем итог всего процесса:

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

See also

Вы можете настроить ответы успешной и неуспешной попытки входа. См. Customizing the Form Login Authenticator Responses.

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

CSRF-атаки входа можно предотвратить, используя ту же технику добавления спрятанных CSRF-токенов в формы входа. Компонент Безопасность уже предоставляет CSRF-защиту, но вам нужно сконфигурировать некоторые опции перед ее использованием.

Для начала, вам нужно подключить CSRF в форме входа:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # config/packages/security.yaml
    security:
        # ...
    
        firewalls:
            secured_area:
                # ...
                form_login:
                    # ...
                    enable_csrf: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="secured_area">
                <!-- ... -->
                <form-login enable-csrf="true"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $mainFirewall = $security->firewall('main');
        $mainFirewall->formLogin()
            // ...
            ->enableCsrf(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 в вашей конфигурации. См. form_login Authentication, чтобы узнать больше.

Вход JSON

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

Включите аутентификтор, используя настройку json_login:

  • YAML
     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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
            <firewall name="main">
                <json-login check-path="api_login"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $mainFirewall = $security->firewall('main');
        $mainFirewall->jsonLogin()
            ->checkPath('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\Annotation\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
  // ...
+ use App\Entity\User;
+ use Symfony\Component\Security\Http\Attribute\CurrentUser;

  class ApiLoginController extends AbstractController
  {
      #[Route('/api/login', name: 'api_login')]
-     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": "[email protected]",
        "password": "MyPassword"
    }
    
  2. Система безопасности перехватывает запрос, проверяет отправленные права доступа пользователя и аутентифицирует его. Если права доступа некорректные, возвращается JSON ответ HTTP 401 Неавторизовано, в других случаях запускается ваш контроллер;

  3. Ваш контроллер создает корреный ответ:

    1
    2
    3
    4
    {
        "user": "[email protected]",
        "token": "45be42..."
    }
    

Tip

Формат JSON-запросов может быть сконфигурирован под ключом json_login. См. JSON Login Authentication, чтобы узнать больше.

Базовый HTTP

Аутентификация базового HTTP - это стандартизированный фреймворк HTTP-аутентификации. Он запрашивает права доступа (имя пользователя и пароль), используя диалог в браузере и аутентификатор базового HTTP Symfony верифицирует эти права.

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # config/packages/security.yaml
    security:
        # ...
    
        firewalls:
            main:
                # ...
                http_basic:
                    realm: Secured Area
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
            <firewall name="main">
                <http-basic realm="Secured Area"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $mainFirewall = $security->firewall('main');
        $mainFirewall->httpBasic()
            ->realm('Secured Area')
        ;
    };
    

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

Note

Вы не можете использовать выход из системы с базовым аутентификатором HTTP. Даже если вы выйдете из Symfony, ваш браузер “помнит” ваши права доступа и будет отправлять их по каждому запросу.

Вход по ссылке

Вход по ссылке - это безпарольный механизм аутентификации. Пользователь будет получать ссылку с коротким сроком жизни (например, по почте), которая аутентифицирует их на веб-сайте.

Вы можете узнать все об этом аутентификаторе в How to use Passwordless Login Link Authentication.

Сертификаты клиентов X.509

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

Для начала, сконфигурируйте ваш веб-сервер, чтобы подключить верификацию сертификатов клиентов, и показать DN сертификатов приложению Symfony:

  • Nginx
     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;
    
            # ...
        }
    }
    
  • Apache
    1
    2
    3
    4
    5
    6
    7
    # ...
    SSLCACertificateFile "/path/to/my-custom-CA.pem"
    SSLVerifyClient optional
    SSLVerifyDepth 1
    
    # передать DN приложению
    SSLOptions +StdEnvVars
    

Затем, включите аутентификатор X.509, используя x509 в вашем брандмауэре:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # config/packages/security.yaml
    security:
        # ...
    
        firewalls:
            main:
                # ...
                x509:
                    provider: your_user_provider
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <!-- ... -->
                <x509 provider="your_user_provider"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $mainFirewall = $security->firewall('main');
        $mainFirewall->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:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/packages/security.yaml
    security:
        firewalls:
            main:
                # ...
                remote_user:
                    provider: your_user_provider
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <firewall name="main">
                <remote-user provider="your_user_provider"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $mainFirewall = $security->firewall('main');
        $mainFirewall->remoteUser()
            ->provider('your_user_provider')
        ;
    };
    

Tip

Вы можете настроить имя этой переменной сервера под ключом remote_user. См. the configuration reference, чтобы узнать больше.

Ограничение попыток входа

New in version 5.2: Регулирование входа было представлено в Symfony 5.2.

Symfony предоставляет базовую защиту от атак входа грубой силы. Вы должны включить ее, используя настройку login_throttling:

  • 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
    # 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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <!-- вы должны использовать менеджер аутентификаторов -->
        <config enable-authenticator-manager="true">
            <!-- ... -->
    
            <firewall name="main">
                <!-- по умолчанию, функция позволяет 5 попыток входа за минуту -->
                <login-throttling/>
    
                <!-- сконфигуровать максимум попыток входа (за минуту) -->
                <login-throttling max-attempts="3"/>
    
                <!-- сконфигурировать максимум попыток входа за заданный период времени -->
                <login-throttling max-attempts="3" interval="15 minutes"/>
    
                <!-- использовать пользовательский ограничитель скорости через его ID сервиса -->
                <login-throttling limiter="app.my_login_rate_limiter"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
    
        $mainFirewall = $security->firewall('main');
    
        // по умолчанию, функция позволяет 5 попыток входа за минуту
        $mainFirewall->loginThrottling();
    
        // сконфигуровать максимум попыток входа (за минуту)
        $mainFirewall->loginThrottling()
            ->maxAttempts(3)
        ;
    
        // сконфигурировать максимум попыток входа за заданный период времени
        $mainFirewall->loginThrottling()
            ->maxAttempts(3)
            ->interval('15 minutes')
        ;
    };
    

New in version 5.3: Опция login_throttling.interval была представлена в Symfony 5.3.

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

Tip

Ограничение неудачных попыток входа - только базовая защита от атак грубой силы. Руководства Атак грубой силы OWASP упоминают несколько других видов защиты, которые вы должны рассмотреть, в зависимости от необходимого уровня безопасности.

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

  • 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
    # 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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            https://symfony.com/schema/dic/symfony/symfony-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <framework:config>
            <framework:rate-limiter>
                <!-- определите 2 ограничителя (один для username+IP, второй - для IP) -->
                <framework:limiter name="username_ip_login"
                    policy="token_bucket"
                    limit="5"
                >
                    <framework:rate interval="5 minutes"/>
                </framework:limiter>
    
                <framework:limiter name="ip_login"
                    policy="sliding_window"
                    limit="50"
                    interval="15 minutes"
                />
            </framework:rate-limiter>
        </framework:config>
    
        <srv:services>
            <!-- наш пользовательский ограничитель -->
            <srv:service id="app.login_rate_limiter"
                class="Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter"
            >
                <!-- 1ый аргумент - ограничитель для IP -->
                <srv:argument type="service" id="limiter.ip_login"/>
                <!-- 2ой аргумент - ограничитель для username+IP -->
                <srv:argument type="service" id="limiter.username_ip_login"/>
            </srv:service>
        </srv:services>
    
        <config>
            <firewall name="main">
                <!-- использовать пользовательский ограничитель по его ID сервиса -->
                <login-throttling limiter="app.login_rate_limiter"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // config/packages/security.php
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
    use Symfony\Config\FrameworkConfig;
    use Symfony\Config\SecurityConfig;
    
    return static function (ContainerBuilder $container, FrameworkConfig $framework, SecurityConfig $security) {
        $framework->rateLimiter()
            ->limiter('username_ip_login')
                ->policy('token_bucket')
                ->limit(5)
                ->rate()
                    ->interval('5 minutes')
        ;
    
        $framework->rateLimiter()
            ->limiter('ip_login')
                ->policy('sliding_window')
                ->limit(50)
                ->interval('15 minutes')
        ;
    
        $container->register('app.login_rate_limiter', DefaultLoginRateLimiter::class)
            ->setArguments([
                // 1ый аргумент - ограничитель для IP
                new Reference('limiter.ip_login'),
                // 2ой аргумент - ограничитель для username+IP
                new Reference('limiter.username_ip_login'),
            ]);
    
        $security->firewall('main')
            ->loginThrottling()
                ->limiter('app.login_rate_limiter')
        ;
    };
    

Выход из системы

Чтобы включить выход из системы, активируйте параметр конфигурации logout под вашим брандмауэром:

  • YAML
     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
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <!-- ... -->
                <logout path="app_logout"/>
    
                <!-- использовать "target", чтобы сконфгируривать, куда перенаправлять после выхода
                <logout path="app_logout" target="app_any_route"/>
                -->
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $mainFirewall = $security->firewall('main');
        // ...
        $mainFirewall->logout()
            ->path('app_logout')
    
            // куда перенаправлять после выхода
            // ->target('app_any_route')
        ;
    };
    

Далее, вам нужно создать маршрут для этого URL (но не контроллер):

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    // src/Controller/SecurityController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\Routing\Annotation\Route;
    
    class SecurityController extends AbstractController
    {
        /**
         * @Route("/logout", name="app_logout", methods={"GET"})
         */
        public function logout(): void
        {
            // контроллер может быть пустым: он не будет вызван!
            throw new \Exception('Don\'t forget to activate logout in security.yaml');
        }
    }
    
  • Attributes
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // src/Controller/SecurityController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\Routing\Annotation\Route;
    
    class SecurityController extends AbstractController
    {
        #[Route('/logout', name: 'app_logout', methods: ['GET'])]
        public function logout()
        {
            // контроллер может быть пустым: он не будет вызван!
            throw new \Exception('Don\'t forget to activate logout in security.yaml');
        }
    }
    
  • YAML
    1
    2
    3
    4
    # config/routes.yaml
    app_logout:
        path: /logout
        methods: GET
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- config/routes.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            https://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="app_logout" path="/logout" methods="GET"/>
    </routes>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // config/routes.php
    use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
    
    return function (RoutingConfigurator $routes) {
        $routes->add('app_logout', '/logout')
            ->methods(['GET'])
        ;
    };
    

Вот и все! Отправив пользователя по маршруту app_logout (т.е. к /logout), Symfony де-аутентифицирует текущего пользователя и перенаправит его.

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

New in version 5.1: LogoutEvent было представлено в Symfony 5.1. До этой версии, вам нужно было использовать обработчик успешного выхода, чтобы настроить выход из системы.

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

getToken()
Вовзвращает токен безопасности, который вот-вот выйдет из системы.
getRequest()
Возвращает текущий запрос.
getResponse()
Возвращает ответ, если он уже установлен пользовательским слушателем. Используйте setResponse(), чтобы сконфигурировать пользовательский ответ выхода.

Извлечение объекта пользователя

После аутентификации, объект User текущего пользователя доступен через ярлык getUser() в базовом контроллере:

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

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:

// src/Service/ExampleService.php
// ...

use Symfony\Component\Security\Core\Security;

class ExampleService
{
    private $security;

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

    public function someMethod()
    {
        // возвращает объект 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:

// src/Entity/User.php

// ...
class User
{
    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    // ...
    public function getRoles(): array
    {
        $roles = $this->roles;
        // гарантировать, что каждый пользователь имеет хотя бы ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }
}

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

  • Каждая роль должна начинаться с ROLE_ (иначе все будет работать не так, как ожидается)
  • Кроме правила выше, роль - это просто строка, и вы можете придумать, что вам нужно
(например, ROLE_PRODUCT_ADMIN).

Вы будете использовать роли далее, чтобы предоставлять доступ к конкретным разделам вашего сайта.

Иерархичные роли

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

  • YAML
    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]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <role id="ROLE_ADMIN">ROLE_USER</role>
            <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $security->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']);
        $security->roleHierarchy('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() вручную. Например, в контроллере, расщиряющемся из базового контроллера:

// ПЛОХО - $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, вы можете:

  • YAML
     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 }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <!-- ... -->
            </firewall>
    
            <!-- требовать ROLE_ADMIN для /admin* -->
            <rule path="^/admin" role="ROLE_ADMIN"/>
    
            <!-- или требовать ROLE_ADMIN или IS_AUTHENTICATED_FULLY для /admin* -->
            <rule path="^/admin">
                <role>ROLE_ADMIN</role>
                <role>IS_AUTHENTICATED_FULLY</role>
            </rule>
    
            <!-- значение 'path' может быть любым валидным регулярным выражением
                 (это будет совпадать с URL вроде /api/post/7298 и /api/comment/528491) -->
            <rule path="^/api/(post|comment)/\d+$" role="ROLE_USER"/>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
    
        // ...
        $security->firewall('main')
        // ...
        ;
    
        // требовать ROLE_ADMIN для /admin*
        $security->accessControl()
            ->path('^/admin')
            ->roles(['ROLE_ADMIN']);
    
        // или требовать ROLE_ADMIN или IS_AUTHENTICATED_FULLY для /admin*
        $security->accessControl()
            ->path('^/admin')
            ->roles(['ROLE_ADMIN', 'IS_AUTHENTICATED_FULLY']);
    
        // значение 'path' может быть любым валидным регулярным выражением
        // (это будет совпадать с URL вроде /api/post/7298 и /api/comment/528491)
        $security->accessControl()
            ->path('^/api/(post|comment)/\d+$')
            ->roles(['ROLE_USER']);
    };
    

Вы можете определить столько паттернов URL, сколько вам нужно - каждый будет регулярным выражением. НО, только один будет сопоставлен с каждым запросом: Symfony начинает сверху списка и останавливается, когда находит первое совпадение:

  • YAML
     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 }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <rule path="^/admin/users" role="ROLE_SUPER_ADMIN"/>
            <rule path="^/admin" role="ROLE_ADMIN"/>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
    
        $security->accessControl()
            ->path('^/admin/users')
            ->roles(['ROLE_SUPER_ADMIN']);
    
        $security->accessControl()
            ->path('^/admin')
            ->roles(['ROLE_ADMIN']);
    };
    

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

Каждый access_control может также сопоставляться с IP-адресом, именем хостинга и HTTP-методами. Он также может быть использован для перенаправления пользователя на https версию паттерна URL. См. How Does the Security access_control Work?.

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

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

// 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 (которую вы можете настроить).

Благодаря SensioFrameworkExtraBundle, вы также можете защитить ваш контроллер, используя аннотации:

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

+ use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

+ /**
+  * Требовать ROLE_ADMIN для *каждого* метода контроллера в этом классе.
+  *
+  * @IsGranted("ROLE_ADMIN")
+  */
  class AdminController extends AbstractController
  {
+     /**
+      * Требовать ROLE_ADMIN только для этого метода контроллера.
+      *
+      * @IsGranted("ROLE_ADMIN")
+      */
      public function adminDashboard(): Response
      {
          // ...
      }
  }

Чтобы узнать больше, см. документацию FrameworkExtraBundle.

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

Если вы хотите проверить, имеет ли текущий пользователь определенную роль, вы можете использовать встроенную хелпер-функцию 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
27
28
  // src/SalesReport/SalesReportManager.php

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

  class SalesReportManager
  {
+     private $security;

+     public function __construct(Security $security)
+     {
+         $this->security = $security;
+     }

      public function generateReport()
      {
          $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, чтобы исключить некоторые маршруты для неаутентифицированного доступа (например, страницу входа):

  • YAML
     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 }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <config enable-authenticator-manager="true">
            <!-- ... -->
    
            <access-control>
                <!-- разрешить неаутентифицированным полльзователям доступ к форме входа -->
                <rule path="^/admin/login" role="PUBLIC_ACCESS"/>
    
                <!-- но требовать аутентификации для всех других админских маршрутов -->
                <rule path="^/admin" role="ROLE_ADMIN"/>
            </access-control>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // config/packages/security.php
    use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
        // ....
    
        // разрешить неаутентифицированным полльзователям доступ к форме входа
        $security->accessControl()
            ->path('^/admin/login')
            ->roles([AuthenticatedVoter::PUBLIC_ACCESS])
        ;
    
        // но требовать аутентификации для всех других админских маршрутов
        $security->accessControl()
            ->path('^/admin')
            ->roles(['ROLE_ADMIN'])
        ;
    };
    

Разрешение доступа анонимным пользователям в пользовательском избирателе

Если вы используете пользовательский избиратель, вы можете разрешить анонимным пользователям доступ, проверив, не установлен ли в токене пользователь:

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

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

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

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

Если вы хотите проверить только выполнил ли пользователь вход (но вам не важны роли), у вас есть два варианта. Первый, если вы дали каждому пользователю ROLE_USER, вы можете проверить эту роль. Другой - вы можете использовать специальный “атрибут” вместо роли:

// ...

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

    // ...
}

Вы можете использовать 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: Когда текущий пользователь изображает другого пользователя в этой сессии, атрибут будет совпадать.

New in version 5.1: Атрибуты IS_REMEMBERED и IS_IMPERSONATOR были представлены в Symfony 5.1.

Deprecated since version 5.3: Атрибуты IS_ANONYMOUS и IS_AUTHENTICATED_ANONYMOUSLY устарели в Symfony 5.3.

Понимание обновления пользователей из сессии

В конце каждого запроса (кроме случаев, когда ваш брандмауэр stateless), ваш объект User сериализируется в сессию. В начале следующего запроса, он десериализируется и затем передается вашему поставщику пользователей для “обновления” (например, запросов Doctrine для свежего пользователя).

Затем, два объектаUser (изначальный из сессии и обновленный объект User) “сравниваются”, чтобы увидеть “равны” ли они. По умолчанию, базовый класс AbstractToken сравнивает возвратные значения методов getPassword(), getSalt() и getUserIdentifier(). Если какие-либо из них отличаются, ваш пользователь выйдет из системы. Это мера безопасности, чтобы гарантировать, что зловредные пользователи будут деаутентифицированы, если изменятся базовые данные пользователя.

Однако, в некоторых случаях, этот процесс может вызвать неожиданные проблемы аутентификации. Если у вас проблемы с аутентификацией, это может быть потому, что вы успешно аутентифицируетесь, но сразу же теряете аутентиикацию после первого перенаправления.

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

Сравнение пользователей вручную с EquatableInterface

Или, если вам нужно больше контроля над процессом “сравнения пользователей”, сделайте так, чтобы ваш класс User реализовывал EquatableInterface. После этого, ваш метод isEqualTo() будет вызываться при сравнении пользователей, вместо базовой логики.

События безопасности

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

Tip

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\CustomLogoutSubscriber:
            tags:
                - name: kernel.event_subscriber
                  dispatcher: security.event_dispatcher.main
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <service id="App\EventListener\CustomLogoutSubscriber">
                <tag name="kernel.event_subscriber"
                     dispacher="security.event_dispatcher.main"
                 />
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\EventListener\CustomLogoutListener;
    use App\EventListener\CustomLogoutSubscriber;
    use Symfony\Component\Security\Http\Event\LogoutEvent;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(CustomLogoutSubscriber::class)
            ->tag('kernel.event_subscriber', [
                'dispatcher' => 'security.event_dispatcher.main',
            ]);
    };
    

События аутентификации

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

Другие события

LogoutEvent
Запускается прямо перед тем, как пользователь выходит из вашего приложения. См. Logging Out.
TokenDeauthenticatedEvent
Запускается когда пользователь деаутентифицирован, например, из-за изменения пароля. См. Understanding how Users are Refreshed from the Session.
SwitchUserEvent
Запускается после завершения “имитации другого”. См. How to Impersonate a User.

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

У меня может быть много брандмауэров?
Да! Но обычно это не нужно. Каждый брандмауэр - это как отдельная система безопасности, аутентификации в одном не делает вас аутентифицированным в другом. Один брандмауэр может иметь множество способов разрешения аутентификации (например, форму входа, аутентификацию ключа API и LDAP).
Могу ли я обмениваться аутентификацией между брандмауэрами?
Да, но только с некоторой конфигурацией. Если вы используете несколько брандмауэров, и вы аутентифицируетесь в одном, вы не будете аутентифицированы в любом другом автоматически. Чтобы сделать это, вам понадобится ясно указать одинаковый Firewall Context для разных брандмауэров. Но обычно для большинства приложений, одного основного брандмауэра достаточно.
Безопасность, похоже, не работает на моих страницах ошибок
Так как маршрутизация проводится до безопасности, страницы ошибок 404 не охватываются ни одним брандмауэром. Это означает, что вы не можете проверять безопасность или даже получить доступ к объекту пользователя на таких страницах. См. How to Customize Error Pages, чтобы узнать больше.
Похоже, моя аутентификация не работает: ошибок нет, но я не могу войти
Иногда аутентификация может быть успешной, но после перенаправления вы сразу же выходите из системы, из-за проблемы с загрузкой User из сессии. Чтобы увидеть, в этом ли проблема, проверьте ваш файл логов (var/log/dev.log) на предмет сообщения логов:
Не могу обновить токен, так как пользователь изменился
Если вы видите это, есть два варианта, почему это так. Во-первых, может быть проблема с загрузкой вашего Пользователя из сессии. См. Understanding how Users are Refreshed from the Session. Во-вторых, если определенная информация пользователя изменилась в БД с момента последнего обновления страницы, Symfony специально выведет пользователя из системы из соображений безопасности.

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