Блог Андрея

 
 

Перевод раздела документации Security Symfony 5.0

Оригинал статьи

Система безопасности Symfony невероятно мощная, но она также может сбивать с толку. Не беспокойтесь!

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

Установка поддержки безопасности;

Создание класса пользователя;

Аутентификация и фаерволы;

Запрещение доступа, роли и прочее об авторизации;

Получение текущего объекта пользователя;

Несколько других важных тем обсуждаются позже.

Установка

В приложении использующим Symfony Flex запустите команду для установки функции безопасности перед её использованием:

composer require symfony/security-bundle

2a) Создайте свой класс пользователя

Независимо от того, как вы будете проходить аутентификацию (например, использовать форму входа или токены API) или где будут храниться ваши пользовательские данные (база данных, «единый вход» (single sign-on) ), следующий шаг всегда одинаков: создайте класс «Пользователь». Самый простой способ - использовать MakerBundle.

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

php bin/console make:user

The name of the security user class (e.g. User) [User]:
> User

Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes

Enter a property name that will be the unique "display" name for the user (e.g.
email, username, uuid [email]
> email

Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes

created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml

Вот и всё! Команда задаёт несколько вопросов, чтобы она могла генерировать именно то, что вам нужно. Наиболее важным является сам файл User.php. Единственное правило о вашем классе User - это то, что он должен реализовывать UserInterface. Не стесняйтесь добавлять любые другие поля или логику, которые вам нужны. Если ваш класс User является сущностью (как в этом примере), вы можете использовать команду make: entity, чтобы добавить больше полей. Также убедитесь, что сделали и запустите миграцию для нового объекта:

php bin/console make:migration
php bin/console doctrine:migrations:migrate

2b) The «User Provider»

В дополнение к вашему пользовательскому классу вам также нужен «пользовательский провайдер»: класс, который помогает с несколькими вещами, такими как перезагрузка пользовательских данных из сессии, и некоторыми дополнительными функциями, такими как «запомнить меня» и олицетворение (impersonation).

К счастью, команда make: user уже настроила ее для вас в файле security.yaml под ключом провайдера.

Это может выглядеть так:

security:
    # https://symfony.com/doc/5.0/security.html#where-do-users-come-from-user-providers
    providers:
        in_memory: { memory: null }
        users:
            entity:
                class: App\Entity\Ausers #Сущность вообще-то может называться как угодно
                property: username

Если ваш класс User является сущностью, вам больше ничего не нужно делать. Но если ваш класс не является сущностью, то make: user также сгенерирует класс UserProvider, который вам нужно завершить. Узнайте больше о пользовательских провайдерах здесь: User Providers.

2c) Encoding Passwords (Шифрование пароля)

Не во всех приложениях есть «пользователи», которым нужны пароли. Если у ваших пользователей есть пароли, вы можете контролировать, как эти пароли кодируются в security.yaml. Команда make: user предварительно настроит это для вас:

# config/packages/security.yaml
security:
    # ...
    encoders:
        # use your user class name here
        App\Entity\User:
            # Use native password encoder
            # This value auto-selects the best possible hashing algorithm
            # (i.e. Sodium when available).
            algorithm: auto

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

Например, используя DoctrineFixturesBundle, вы можете создать фиктивных пользователей базы данных Фисктуры используются для создания наборов данных для автоматических тестов - прим. переводчика.:

php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> UserFixtures

Используйте UserPasswordEncoderInterface для шифрования пароля:

// src/DataFixtures/UserFixtures.php

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
// ...

class UserFixtures extends Fixture
{
     private $passwordEncoder;

     public function __construct(UserPasswordEncoderInterface $passwordEncoder)
     {
         $this->passwordEncoder = $passwordEncoder;
     }

    public function load(ObjectManager $manager)
    {
        $user = new User();
        // ...

         $user->setPassword($this->passwordEncoder->encodePassword(
             $user,
             'the_new_password'
         ));

        // ...
    }
}

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

php bin/console security:encode-password

3a) Аутентификация и фаерволы

Система безопасности настраивается в config/packages/security.yaml. Самая важная секция это firewalls:

# config/packages/security.yaml
security:
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: lazy

«Брандмауэр (firewall)» - это ваша система аутентификации: приведенная ниже конфигурация определяет, как ваши пользователи смогут проходить аутентификацию (например, форма входа, токен API и т. д.).

На каждый запрос активен только один брандмауэр: Symfony использует ключ шаблона, чтобы найти первое совпадение (вы также можете сопоставить его по хосту или по другим причинам). Брандмауэр dev на самом деле является фальшивым брандмауэром: он просто гарантирует, что вы случайно не заблокируете инструменты разработки Symfony, которые находятся под такими URL-адресами, как /_profiler и /_wdt.

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

На самом деле, если вы перейдете на домашнюю страницу прямо сейчас, у вас будет доступ, и вы увидите, что вы «аутентифицированы» как anon… Не дайте себя одурачить значению «Да» рядом с Authenticated. Брандмауэр подтвердил, что он не знает вашу личность, поэтому вы анонимны:

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

composer require --dev symfony/profiler-pack

Также дело может быть в том, что вы используете prod окружение в файле .env.

Позже вы узнаете, как запретить доступ к определенным URL-адресам или контроллерам.

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

3b) Авторизация ваших пользователей

Поначалу аутентификация в Symfony может показаться немного «волшебной». Это потому, что вместо создания маршрута и контроллера для обработки входа в систему вы активируете провайдера аутентификации: некоторый код, который запускается автоматически перед вызовом вашего контроллера.

Symfony имеет несколько встроенных провайдеров(поставщиков) аутентификации. Если ваш вариант использования точно соответствует одному из них, отлично! Но в большинстве случаев - включая форму входа в систему - мы рекомендуем создать Guard Authenticator: класс, который позволяет вам контролировать каждую часть процесса аутентификации (см. Следующий раздел).

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

Guard Authenticators

Guard Authenticator - это класс, который даёт вам полный контроль над процессом аутентификации. Существует много разных способов создания аутентификатора, поэтому вот несколько распространенных вариантов использования:

Как создать форму входа

Система пользовательской аутентификации с защитой (пример API-токена)

Для получения более подробного описания средств проверки подлинности и их работы см. Система настраиваемой проверки подлинности с защитой (пример API-токена).

4) Запрещение доступа, роли и прочее об авторизации

Теперь пользователи могут войти в ваше приложение, используя вашу форму входа. Теперь вам нужно узнать, как запретить доступ и работать с объектом User. Это называется авторизацией, и его задача - решить, может ли пользователь получить доступ к какому-либо ресурсу (URL, объект модели, вызов метода, ...).

Пользователь получает определенный набор ролей при входе в систему (например, ROLE_ADMIN).

Вы добавляете код так, чтобы ресурсу (например, URL, контроллеру) требовался определенный «атрибут» (чаще всего роль, например, ROLE_ADMIN) для доступа.

Роли

Когда пользователь входит в систему, Symfony вызывает метод getRoles() для вашего объекта User, чтобы определить, какие роли выполняет этот пользователь. В классе User, который мы сгенерировали ранее, роли - это массив, который хранится в базе данных, и каждому пользователю всегда предоставляется по крайней мере одна роль: ROLE_USER:

// src/Entity/User.php
// ...

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

public function getRoles(): array
{
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
}

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

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

Добавить код для запрета доступа

Есть два способа запретить доступ к чему-либо:

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

Защита шаблонов URL (access_control)

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


# config/packages/security.yaml
security:
    # ...

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

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: '^/admin', roles: ROLE_ADMIN }

        # or require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin*
        - { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }

        # the 'path' value can be any valid regular expression
        # (this one will match URLs like /api/post/7298 and /api/comment/528491)
        - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }

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

# config/packages/security.yaml
security:
    # ...

    access_control:
        # совпадение /admin/users/*
        - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }

        # совпадение /admin/* за исключением всего, что соответствует вышеуказанному правилу
        - { path: '^/admin', roles: ROLE_ADMIN }

Каждый access_control может также соответствовать по IP-адресу, имени хоста и HTTP-методам. Его также можно использовать для перенаправления пользователя на https-версию шаблона URL. Посмотрите, как работает Security access_control?

Защита в контроллерах и в другогом коде

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

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

public function adminDashboard()
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');
    // or add an optional message - seen by developers
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
}

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

  1. Если пользователь еще не вошел в систему, ему будет предложено войти (например, перенаправлено на страницу входа).
  2. Если пользователь вошел в систему, но не имеет роли ROLE_ADMIN, ему будет показана страница с отказом в доступе 403 (которую вы можете настроить).

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

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

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

 /**
  * Require ROLE_ADMIN for *every* controller method in this class.
  *
  * @IsGranted("ROLE_ADMIN")
  */
class AdminController extends AbstractController
{
     /**
      * Require ROLE_ADMIN for only this controller method.
      *
      * @IsGranted("ROLE_ADMIN")
      */
    public function adminDashboard()
    {
        // ...
    }
}

Для получения дополнительной информации см. Документацию FrameworkExtraBundle.

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

Если вы хотите проверить, имеет ли текущий пользователь определённую роль, вы можете использовать встроенную функцию is_granted() в любом шаблоне twig:

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

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

See How to Secure any Service or Method in your Application.

Проверка, вошел ли пользователь в систему (IS_AUTHENTICATED_FULLY)

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

Во-первых, если вы дали каждому пользователю ROLE_USER, вы можете просто проверить эту роль.

В противном случае вы можете использовать специальный атрибут вместо роли:


// ...
public function adminDashboard()
{
    $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

    // ...
}

Вы можете использовать IS_AUTHENTICATED_FULLY везде, где используются роли: например, в секции access_control конфигурации или в шаблоне Twig.

IS_AUTHENTICATED_FULLY не является ролью, но она ведет себя пододбно им и каждый зарегистрированный пользователь как бы имеет её. На самом деле, есть 3 специальных атрибута:

  • IS_AUTHENTICATED_REMEMBERED: это есть у всех вошедших в систему пользователей, даже если они вошли в систему из-за файла cookie "запомнить меня". Даже если вы не используете функцию "запомнить меня", вы можете использовать это, чтобы проверить, вошел ли пользователь в систему.
  • IS_AUTHENTICATED_FULLY: это похоже на IS_AUTHENTICATED_REMEMBERED, но сильнее. Пользователи, которые вошли в систему только из-за файла cookie cookie, будут иметь IS_AUTHENTICATED_REMEMBERED, но не будут иметь IS_AUTHENTICATED_FULLY.
  • IS_AUTHENTICATED_ANONYMOUSLY: Все пользователи (даже анонимные) имеют это - это полезно, когда URL-адреса в белых списках гарантируют доступ - некоторые подробности приведены в разделе Как работает security access_control ?.

Списки контроля доступа (ACL (Access Control List) ): защита отдельных объектов базы данных

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

"Избиратели" (Voters) позволяют вам написать любую бизнес-логику, которая вам нужна (например, пользователь может редактировать этот пост, потому что он является его создателем), чтобы определить доступ. Вот почему Symfony официально рекомендует избирателей создавать ACL-подобные системы безопасности.

Если вы всё еще предпочитаете использовать традиционные списки ACL, обратитесь к комплекту ACL Symfony.

5a) Получение текущего объекта пользователя;

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

public function index()
{
    // обычно вы хотите убедиться, что пользователь прошел аутентификацию первым
    $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

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

    // Вызывайте любые методы, которые вы добавили в свой класс User
    // Например, если вы добавили метод getFirstName(), вы можете использовать это.
    return new Response('Well hi there '.$user->getFirstName());
}

5b) Получение объекта пользователя из сервиса

Если вам нужно получить зарегистрированного пользователя из службы, используйте сервис Security:

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

use Symfony\Component\Security\Core\Security;

class ExampleService
{
    private $security;

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

    public function someMethod()
    {
        // возвращает объект User или null, если не аутентифицирован
        $user = $this->security->getUser();
    }
}

Выбрать пользователя в шаблоне

В Twig шаблоне объект пользователя доступен в переменной app.user благодаря Глобальной переменной Twig:

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

Выход

Для доступности выхода из системы, активируйте logout в секции конфигурации вашего firewall-а:


# config/packages/security.yaml
security:
    # ...

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

                # where to redirect after logout
                # target: app_any_route

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

// 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('Не забудьте активировать logout в security.yaml');
    }
}

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

Нужно больше контроля над тем, что происходит после выхода из системы? Добавьте ключ success_handler при выходе из системы и укажите его для идентификатора службы класса, который реализует LogoutSuccessHandlerInterface.

Иерархия ролей

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

# 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).

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

// ПЛОХО! - $user->getRoles() не будет знать об иерархии ролей
$hasAccess = in_array('ROLE_ADMIN', $user->getRoles());

// Правильный подход
$hasAccess = $this->isGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('ROLE_ADMIN');

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

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

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

Могу я иметь несколько фаерволов (брандмауэров)?

Да! Но обычно это не нужно. Каждый брандмауэр похож на отдельную систему безопасности. И поэтому, если у вас совсем другие потребности в аутентификации, один брандмауэр обычно работает хорошо. С помощью аутентификации Guard вы можете создавать различные способы разрешения аутентификации (например, входа в систему, аутентификации с помощью ключа API и LDAP) под одним и тем же брандмауэром.

Могу ли я пройти аутентификацию сразу в нескольких брандмауэрами?

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

Безопасность не работает на моих страницах ошибок

Поскольку маршрутизация выполняется до обеспечения безопасности, страницы ошибок 404 не защищены брандмауэром. Это означает, что вы не можете проверить безопасность или даже получить доступ к объекту пользователя на этих страницах. См. Как настроить страницы ошибок для более подробной информации.

Моя аутентификация не работает: ошибок нет, но я не могу войти

Иногда аутентификация может быть успешной, но после перенаправления вы сразу же выходите из системы из-за проблемы с загрузкой объекта User из сессии. Чтобы увидеть, существует ли эта проблема, проверьте ваш файл журнала (var/log /dev.log) для сообщения журнала.

Иногда это может быть просто из-за отсутствия favicon на сайте, а запрос к нему при обработке html браузером отправляется - примечание переводчика.

Невозможно обновить токен, потому что пользователь изменился

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

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

Аутентификация

Авторизация (Ограничение прав)