Как работает безопасность access_control?

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

Каждый access_control имеет несколько опций, которые конфигурируют две разных вещи:

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

1. Опции совпадения

Symfony создаёт экземпляр класса RequestMatcher для каждой записи access_control, который определяет должно ли быть использовано данное управлеие доступом в этом запросе. Слелдующие опции access_control используются для сопоставления:

  • path
  • ip или ips
  • host
  • methods

Возьмите следующие записи access_control в качестве примера:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
            - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ }
            - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
            - { path: ^/admin, roles: ROLE_USER }
    
  • 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>
            <!-- ... -->
            <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
            <rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" />
            <rule path="^/admin" role="ROLE_USER_METHOD" methods="POST, PUT" />
            <rule path="^/admin" 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
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_IP',
                'ip' => '127.0.0.1',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_HOST',
                'host' => 'symfony\.com$',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_METHOD',
                'methods' => 'POST, PUT',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER',
            ),
        ),
    ));
    

Для каждого входящего запроса, Symfony будет решать, какой access_control использовать, основываясь на URI, IP-адресе клиента, имени входящего хоста и методе запроса. Помните, используется первое совпадающее правило, и если для записи не указаны ip, host или method, то этот access_control совпадёт с любым ip, host или method:

URI IP ХОСТ МЕТОД access_control Почему?
/admin/user 127.0.0.1 example.com GET правило #1 (ROLE_USER_IP) URI совпадает с path, а IP - с ip.
/admin/user 127.0.0.1 symfony.com GET
правило #1 (ROLE_USER_IP) | path и ip всё ещё совпадают. Это также будет
совпадать с записью ROLE_USER_HOST, но используется
только первое совпадение access_control.
/admin/user 168.0.0.1 symfony.com GET
правило #2 (ROLE_USER_HOST) | ip не совпадает с первым правилом, так что используется
второе (совпадающее) правило.
/admin/user 168.0.0.1 symfony.com POST правило #2 (ROLE_USER_HOST) Второе правило всё ещё совпадает. Это также будет совпадать с третьим правилом (ROLE_USER_METHOD), но используется только первое совпадение access_control.
/admin/user 168.0.0.1 example.com POST правило #3 (ROLE_USER_METHOD) ip и host не совпадают с первыми двумя записями, но совпадают с третьей - ROLE_USER_METHOD (она используется).
/admin/user 168.0.0.1 example.com GET правило #4 (ROLE_USER) ip, host и method предотвращают совпадение с первыми тремя записями. Но так как URI совпадает с шаблоном path записи ROLE_USER, она используется.
/foo 127.0.0.1 symfony.com POST не совпадает ни с одной записью Не совпадает ни с одним правилом access_control так как его URI не совпадает ни с одним значением path.

2. Форсирование доступа

После того, как Symfony решила, какая запись access_control совпадает (если таковая есть), она форсирует ограничения доступа, основанные на опциях roles, allow_if и requires_channel:

  • roles Если пользователь не имеет данной(ых) роли(ей), то в доступе будет отказано (внутренне, вызывается AccessDeniedException);
  • allow_if Если выражение возвращает "неверно", то в доступе будет отказано;
  • requires_channel Если канал входящего запроса (например, http) не совпадает с этим значением (например, https), пользователь будет перенаправлен (например, перенаправлен с http на https, или наоборот).

Tip

Если в доступе отказано, система попробует аутентифицировать пользователя, если это ещё не было сделано (например, перенаправить ползователя на страницу входа). Если пользователь уже выполил вход, будет показана страница ошибки 403 "доступ запрещён". Смотрите How to Customize Error Pages, чтобы узнать больше.

Соответствие access_control по IP

Некоторые ситуации могут возникнуть, когда вам нужна запись access_control, которая совпадает только с запросами, исходящими от какого-то IP-адреса или их спектра. Например, это может быть использовано, для отказа в доступе к URL-шаблону для всех запросов, кроме тех, что исходят от внутреннего доверенного сервера.

Caution

Как вы прочтёте в объяснении под примером, опция ips не ограничивается конкретным IP-адресом. Вместо этого, использование ключа ips означает, что запись access_control будет совпадать только с этим IP-адресом, а пользователи, получающие доступ к ней с других IP-адресов, будут идти дальше по списку access_control.

Вот пример того, как вы можете сконфигурировать некоторый шаблон URL /internal* так, чтобы он был доступен только по запросам с локального сервера:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    security:
        # ...
        access_control:
            #
            - { path: ^/internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
            - { path: ^/internal, roles: ROLE_NO_ACCESS }
    
  • 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>
            <!-- ... -->
            <rule path="^/internal"
                role="IS_AUTHENTICATED_ANONYMOUSLY"
                ips="127.0.0.1, ::1"
            />
    
            <rule path="^/internal" role="ROLE_NO_ACCESS" />
        </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(
        // ...
        'access_control' => array(
            array(
                'path' => '^/internal',
                'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
                'ips' => '127.0.0.1, ::1'
            ),
            array(
                'path' => '^/internal',
                'role' => 'ROLE_NO_ACCESS'
            ),
        ),
    ));
    

Вот, как это работает, когда путь - /internal/something исходит от внешнего IP-адреса 10.0.0.1:

  • Первое правило контроля доступа игнорируется, так как path совпадает, но IP-адреса не совпадают ни с одним из перечисленных IP;
  • Включается второе правило контроля доступа (единственное ограничение - path) и оно совпадает. Если вы убедитесь в том, что ни один пользователь не имеет ROLE_NO_ACCESS, то в доступе будет отказано (ROLE_NO_ACCESS может быть чем угодно, что не совпадает с существующей ролью, оно просто служит способом всегда отказывать в доступе).

Но если тот же запрос поступит от 127.0.0.1 или ::1 (адрес обратной связи IPv6):

  • Теперь, первое правило контроля доступа включается, так как совпадает и path и ip: доступ разрешён, так как пользователь всегда имеет роль IS_AUTHENTICATED_ANONYMOUSLY.
  • Второе правило контроля доступа не рассматривается, так как совпало первое.

Безопасность по выражению

Когда запись access_control совпадает, вы можете отказать в доступе через ключ roles или использовать более сложную логику с выражением в ключе allow_if:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    security:
        # ...
        access_control:
            -
                path: ^/_internal/secure
                allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')"
    
  • XML
    1
    2
    3
    4
    <access-control>
        <rule path="^/_internal/secure"
            allow-if="'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" />
    </access-control>
    
  • PHP
    1
    2
    3
    4
    5
    6
    'access_control' => array(
        array(
            'path' => '^/_internal/secure',
            'allow_if' => '"127.0.0.1" == request.getClientIp() or has_role("ROLE_ADMIN")',
        ),
    ),
    

В этом случае, когда пользователь пытается получить доступ к любому URL, начинающемуся с /_internal/secure, он его получит только, если IP-адрес - 127.0.0.1, или если он имеет роль ROLE_ADMIN.

Внутри выражения, у вас есть доступ к нескольким разным переменным и функциям, включая request, которая является объектом Symfony Request (смотрите Запрос).

Чтобы увидеть список других функций и переменных, смотрите functions and variables.

Форсирование канала (http, https)

Вы также можете обязать пользователя получать доступ к URL через SSL; просто используйте аргумент requires_channel в любых записях access_control. Если access_control совпадёт, и запрос использует канал http, то пользователь будет перенаправлен на https:

  • YAML
    1
    2
    3
    4
    5
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- 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">
    
        <rule path="^/cart/checkout"
            role="IS_AUTHENTICATED_ANONYMOUSLY"
            requires-channel="https"
        />
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'access_control' => array(
            array(
                'path' => '^/cart/checkout',
                'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
                'requires_channel' => 'https',
            ),
        ),
    ));
    

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