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

Система безопасности Symfony невероятно мощная, но в то же время может быть сложной в установке. В этой главе вы научитесь устанавливать безопасность вашего приложения шаг за шагом; от конфигурации вашего фаервола и загрузки пользователей до отказа в доступе и вызова объекта User. В зависимости от того, что вам нужно, иногда начальная установка может быть непростой. Но как только вы это сделаете, система безопасности Symfony станет и гибкой, и (надеемся) простой для работы.

Так как нам предстоит о многом поговорить, эта глава поделена на несколько больших разделов:

  1. Начальная установка security.yml (аутентификация);
  2. Отказ в доступе к вашему приложению (авторизация);
  3. Вызов текущего объекта User.

За этими разделами последуют маленькие (но все равно захватывающие) подразделы вроде выхода из системы и шифрования пользовательских паролей.

1) Начальная установка security.yml (аутентификация)

Система безопасности конфигурируется в app/config/security.yml. Конфигурация по умолчанию выглядит так:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory: ~
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
    
            main:
                anonymous: ~
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory />
            </provider>
    
            <firewall name="dev"
                pattern="^/(_(profiler|wdt)|css|images|js)/"
                security="false" />
    
            <firewall name="main">
                <anonymous />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => null,
            ),
        ),
        'firewalls' => array(
            'dev' => array(
                'pattern'   => '^/(_(profiler|wdt)|css|images|js)/',
                'security'  => false,
            ),
            'main' => array(
                'anonymous' => null,
            ),
        ),
    ));
    

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

Tip

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

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

_images/anonymous_wdt.png

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

Tip

Безопасность имеет массу возможностей конфигурации, и существует Справочник конфигурации безопасностиe, в котором указывают все опции с дополнительными разъяснениями.

A) Конфигурирование аутентификации ваших пользователей

Главной задачей брандмауэра является конфигурирование того, как будут аутентифицироваться ваши пользователи. Они будут использовать форму логина? Базовую аутентификацию HTTP? Маркер API? Все вышеперечисленное?

Давайте начнем с базовой аутентификации HTTP (старомодные подсказки (prompt)) и будем постепенно углубляться. Чтобы активировать это, добавьте в ваш брандмауэр ключ http_basic:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            # ...
            main:
                anonymous: ~
                http_basic: ~
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <anonymous />
                <http-basic />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'main' => array(
                'anonymous'  => null,
                'http_basic' => null,
            ),
        ),
    ));
    

Легко! Чтобы попробовать это, вам понадобится, заставить пользователя войти в систему, чтобы увидеть страницу. Чтобы сделать все интереснее, создайте новую страницу /admin. Например, если вы используете аннотации, создайте что-то вроде этого:

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

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * @Route("/admin")
     */
    public function adminAction()
    {
        return new Response('<html><body>Admin page!</body></html>');
    }
}

Теперь, добавьте запись access_control в security.yml, которая будет требовать, чтобы пользовать вошел в систему для доступа к этому URL:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # app/config/security.yml
    security:
        # ...
        firewalls:
            # ...
            main:
                # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { 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
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <!-- ... -->
            </firewall>
    
            <!-- require ROLE_ADMIN for /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
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'main' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'roles' => 'ROLE_ADMIN'),
        ),
    ));
    

Note

Вы узнаете больше о ROLE_ADMIN и отказе в доступе позже в разделе Отказ в доступе, роли и другая авторизация security-authorization.

Отлично! Теперь, если вы перейдете на /admin, вы увидите, базовую подсказку аутентификации HTTP:

_images/http_basic_popup.png

Но как кто вы можете выполнить вход? Откуда приходят пользователи?

Tip

Хотите использовать традиционную форму входа? Отлично! Смотрите Как построить традиционную форму входа. Какие еще методы поддерживаются? Смотрите Справочник конфигурации или создайте сами.

Tip

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

B) Конфигурирование загрузки пользователей

Когда вы вводите свое имя пользователя, Symfony должен откуда-то загрузить информацию этого пользователя. Это называется «провайдером пользователя», и когда вы отвечаете за его конфигурирование, Symfony имеет встроенный способ загружать пользователей из БД, или же вы можете сами создать провайдер пользователя.

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: ryanpass
                            roles: 'ROLE_USER'
                        admin:
                            password: kitten
                            roles: 'ROLE_ADMIN'
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                    <user name="admin" password="kitten" roles="ROLE_ADMIN" />
                </memory>
            </provider>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => 'ryanpass',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => 'kitten',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

Как и с firewalls, у вас может быть несколько providers, но скорее всего вам понадобится только один. Если же у вас будет несколько, вы можете сконфигурировать, какой провайдер стоит использовать вашему брандмауэру в ключе provider (например, provider: in_memory).

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

Попробуйте войти в систему, используя имя пользователя admin и пароль kitten. Вы должны увидеть ошибку!

No encoder has been configured for account "Symfony\Component\Security\Core\User\User" (Для аккаунта "SymfonyComponentSecurityCoreUserUser" не было создано кодировщика)

Чтобы исправить это, добавьте ключ encoders:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="plaintext" />
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => 'plaintext',
        ),
        // ...
    ));
    

Провайдеры пользователя загружают информацию пользователя и помещают ее в объект User. Если вы загружаете пользователей из БД или какого-то другого источника, вы будете использовать ваш личный класс User. Но при использовании провайдера «в памяти», вы получаете объект Symfony\Component\Security\Core\User\User.

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

Если вы обновите страницу сейчас, вы будете в системе! Тулбар веб-отладки даже сообщает вам кто вы, и какие роли имеете:

_images/symfony_loggedin_wdt.png

Так как этот URL требует ROLE_ADMIN, если бы вы вошли как ryan, вам было бы отказано в доступе. Больше вы узнаете дальше (Безопасность шаблонов URL (access_control)).

Загрузка пользователей из БД

Если вы хотите загружать ваших пользователей через ORM Doctrine, то это очень легко сделать! Смотрите Как загружать безопасных пользователей из БД, чтобы узнать все детали.

C) Шифрование пароля пользователя

Где бы ни хранились ваши пользователи, в security.yml, базе данных, или где-либо еще, вы захотите зашифровать их пароли. Лучшим алгоритмом для использования является bcrypt:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm: bcrypt
                cost: 12
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="bcrypt"
                cost="12" />
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => array(
                'algorithm' => 'bcrypt',
                'cost' => 12,
            )
        ),
        // ...
    ));
    

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

1
$ php bin/console security:encode-password

Она выдаст вам что-то такое:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    # app/config/security.yml
    security:
        # ...
    
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
                            roles: 'ROLE_USER'
                        admin:
                            password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
                            roles: 'ROLE_ADMIN'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli" roles="ROLE_USER" />
                    <user name="admin" password="$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G" roles="ROLE_ADMIN" />
                </memory>
            </provider>
        </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
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

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

Tip

Поддерживаемые алгоритмы этого метода зависят от вашей версии PHP, но включают алгоритмы, возвращаемые функцией hash_algos, а также некоторые другие (например, bcrypt). Смотрите ключ encoders в разделе Справочник безопасности» для примеров.

Также возможно использовать разные алгоритмы хеширования на основании пользователь за пользователем. См. Как динамично выбрать алгоритм шифрования пароля.

D) Конфигурация готова!

Поздравляем! Теперь у вас есть работающая система аутентификации, которая использует базовую аутентификацию HTTP и загружает пользователей прямо из файла security.yml.

Ваши следующие шаги зависят от вашей установки:

2) Отказ в доступе, роли и другая авторизация

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

Процесс авторизации имеет две стороны:

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

Tip

Вдобавок к ролям (например, ROLE_ADMIN), вы можете защитить ресурс, используя другие атрибуты/строки (например, EDIT), и использовать избирателя или систему ACL Symfony, чтобы придать им значение. Это может быть удобным, если вам нужно проверить, может ли пользователь А EDIT (править) некий объект Б (например, продукт с id 5). См. Безопасные объекты security-secure-objects. See Access Control Lists (ACLs): Securing individual Database Objects.

Роли

Когда пользователь выполняет вход, он получает набор ролей (например, ROLE_ADMIN). В примере выше, они жестко закодированы в security.yml. Если вы загружаете пользователей из базы данных, они, скорее всего, хранятся в столбце вашей таблицы.

Caution

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

Роли - это просто, и по сути они являются строками, которые вы придумываете и используете так, как вам нужно. Например, если вам нужно начать ограничивать доступ к админской секции блога, вы можете защитить ее, используя роль ROLE_BLOG_ADMIN. Эта роль не должна быть нигде обозначена - вы просто можете начать ее использовать.

Tip

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

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

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

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

  1. access_control в security.yml позволяет вам защитить шаблоны URL (например, /admin/*). Это легкий, но не гибкий способ;
  2. запрет в вашем коде с помощью сервиса security.authorization_checker.

Безопасность шаблонов URL (access_control)

Самым базовым способом защитить часть вашего приложения является защита всего шаблона URL. Вы видели это раньше, когда всему, совпадающему с регулярным выражением ^/admin необходима роль ROLE_ADMIN:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            # ...
            main:
                # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { 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
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="main">
                <!-- ... -->
            </firewall>
    
            <!-- require ROLE_ADMIN for /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
    15
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'firewalls' => array(
            // ...
            'main' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Это прекрасно для защиты целых разделов, но вам также может захотеться обезопасить индивидуальные контроллеры.

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    security:
        # ...
    
        access_control:
            - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-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
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'access_control' => array(
            array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

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

Раздел access_control - очень мощный, но может быть опасным (так как имеет отношение к безопасности), если вы не понимаете принципа его работы. Вдобавок к URL, access_control может совпадать с IP-адресом, именем хоста и методами HTTP. Он также может быть использован для перенаправления пользователя на https версии шаблона URL.

Чтобы узнать обо всем этом, см. Как работает безопасность access_control?.

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ...

public function helloAction($name)
{
    // TВторой параметр используется для указания объекта тестирования роли.
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');

    // Старый способ:
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Unable to access this page!');
    // }

    // ...
}

В обоих случаях, вводится специальный AccessDeniedException, который, в конечном счете, запускает ответ HTTP 403 в Symfony.

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Security("has_role('ROLE_ADMIN')")
 */
public function helloAction($name)
{
    // ...
}

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

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

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

  • Twig
    1
    2
    3
    {% if is_granted('ROLE_ADMIN') %}
        <a href="...">Delete</a>
    {% endif %}
    
  • PHP
    1
    2
    3
    <?php if ($view['security']->isGranted('ROLE_ADMIN')): ?>
        <a href="...">Delete</a>
    <?php endif ?>
    

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

Все в Symfony можно защитить, если сделать с кодом что-то похожее на защиту контроллера. Например, представьте, что у вас есть сревис (т.е. PHP-класс), задачей которого является отправка email’ов. Вы можете ограничить использование этого класса - независимо от того, откуда он используется - для некоторых пользователей.

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

Проверка выполнения входа пользователем (IS_ATHENTICATED_FULLY)

До сих пор, вы проверяли доступ, основываясь на ролях - строках, начинающихся с ROLE_ и приписанных пользователям. Но если вы только хотите проверить, выполнил ли пользователь вход (вас не волнуют роли), вы можете использовать IS_AUTHENTICATED_FULLY:

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

public function helloAction($name)
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

Tip

Конечно, вы также можете использовать это в access_control.

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

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

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

  • Twig
    1
    2
    3
    4
    5
    {% if is_granted(expression(
        '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
    )) %}
        <a href="...">Delete</a>
    {% endif %}
    
  • PHP
    1
    2
    3
    4
    5
    <?php if ($view['security']->isGranted(new Expression(
        '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
    ))): ?>
        <a href="...">Delete</a>
    <?php endif; ?>
    

Для более подробной информации о выражениях и безопасности, смотрите Безопасность: сложный контроль доступа с выражениями expressions-security.

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

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

Для достижения этого у вас есть две опции:

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

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

Извлечение объекта User

После аутентификации, к объекту User текущего пользователя можно получить доступ через сервис security.token_storage . Из контроллера это будет выглядеть так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use Symfony\Component\Security\Core\User\UserInterface;

public function indexAction(UserInterface $user)
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    // вышенаписанное - сокращение для этого
    $user = $this->get('security.token_storage')->getToken()->getUser();
}

Tip

Пользователь будет объектом, а класс этого объекта будет зависеть от вашего провайдера пользователя.

New in version 3.2: Функционал для получения пользователя с помощью метода подписи был представлен в Symfony 3.2. Вы все еще можете вызвать его с помощью $this->getUser(), если вы расширите класс Controller.

Теперь вы можете вызывать любые методы, которые есть в вашем объекте User. Например, если ваш объект User имеет метод getFirstName(), вы можете это использовать:

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Response;
// ...

public function indexAction()
{
    // ...

    return new Response('Well hi there '.$user->getFirstName());
}

Всегда проверяйте, выполнил ли пользователь вход

Очень важно в первую очередь проверять, аутентифицирован ли пользователь. Если нет, тогда $user будет либо null, либо строкой anon.. Стоп, что? Да, вот такая особенность. Если вы не выполнили вход, то пользователь технически является строкой anon., хотя сокращение контроллера getUser() конвертирует ее в null для удобства. При типизированни класса UserInterface, когда выполнение входа является необязательным, вы можете допустить недействительное значение аргумента:

1
2
3
4
public function indexAction(UserInterface $user = null)
{
    // $user недействителен, когда он не выполнил вход или аноимен.
}

Основная мысль: всегда проверяйте, выполнил ли пользователь вход перед тем, как использовать объект User, и используйте метод isGranted() (или access_control) для этого:

1
2
3
4
5
6
7
8
9
// ура! Используйте это, чтобы увидеть, выполнил ли пользователь вход
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw $this->createAccessDeniedException();
}

//  бу :(. Никогда не проверяйте объект User для проверки выполнения входа
if ($this->getUser()) {

}

Поиск пользователя в шаблоне

В шаблоне Twig доступ к объекту можно получить с помощью ключа app.user:

  • Twig
    1
    2
    3
    {% if is_granted('IS_AUTHENTICATED_FULLY') %}
        <p>Username: {{ app.user.username }}</p>
    {% endif %}
    
  • PHP
    1
    2
    3
    <?php if ($view['security']->isGranted('IS_AUTHENTICATED_FULLY')): ?>
        <p>Username: <?php echo $app->getUser()->getUsername() ?></p>
    <?php endif; ?>
    

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

Caution

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

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            secured_area:
                # ...
                logout:
                    path:   /logout
                    target: /
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="secured_area">
                <!-- ... -->
                <logout path="/logout" target="/" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'logout' => array('path' => '/logout', 'target' => '/'),
            ),
        ),
    ));
    

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

  • YAML
    1
    2
    3
    # app/config/routing.yml
    logout:
        path: /logout
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- app/config/routing.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
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="logout" path="/logout" />
    </routes>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('logout', new Route('/logout'));
    
    return $collection;
    

Вот и все! Перенавпрляя пользователя на /logout (или на любой созданный вами path (путь)), Symfony разаутентифицирует текущего пользователя.

После того, как пользователь выйдет, он будет перенаправлен по пути, обозначенному в параметре target выше (например, на homepage (главную страницу)).

Tip

Если вам нужно сделать что-то более интересное после выхода из системы, вы можете указать обработчик выполнения успешного выхода, добавив ключ success_handler и указав ему сервис-id класса выполняющего LogoutSuccessHandlerInterface. Смотрите Справочник конфигурации безопасности.

Иерархические роли

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    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
    <!-- app/config/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
            http://symfony.com/schema/dic/services/services-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
    10
    11
    12
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array(
                'ROLE_ADMIN',
                'ROLE_ALLOWED_TO_SWITCH',
            ),
        ),
    ));
    

В конфигурации выше, пользователи с ролью ROLE_ADMIN также будут иметь роль ROLE_USER. Роль ROLE_SUPER_ADMIN имеет роли ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH и ROLE_USER (унаследованную от ROLE_ADMIN).

Заключение

Фух, хорошая работа! Теперь вы знаете больше, чем просто основы безопасности. Самые сложные моменты - когда у вас есть специфические требования. Например, специальная стратегия аутентификации (например, маркеры API), сложная логика авторизации и многие другие вещи (потому что безопасность - сложная!).

К счастью, есть много статей о безопасности, которые направлены на то, чтобы описать многие из сложных ситуаций. Также смотрите раздел справочника Безопасность. Многие опции не имеют подробных деталей, но увидеть максимально полное древо конфигурации может быть полезным.

Удачи!

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