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

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

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

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

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

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

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

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

  • path: регулярное выражение (без разграничителей)
  • ip или ips: маски сети также поддерживаются (может быть строкой, разделенной запятой)
  • port: целое число
  • host: регулярное выражение
  • methods: один или несколько методов
  • request_matcher: сервис, реализующий RequestMatcherInterface
  • attributes: массив, который может быть использован, чтобы указать один или более атрибутов запроса , которые должны совпадать точно
  • route: имя маршрута

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

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.yaml
parameters:
    env(TRUSTED_IPS): '10.0.0.1, 10.0.0.2'

security:
    # ...
    access_control:
        - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 }
        - { 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] }

        # ip могут быть разделены запятой, что особенно полезно при использовании переменных окружения
        - { path: '^/admin', roles: ROLE_USER_IP, ips: '%env(TRUSTED_IPS)%' }
        - { path: '^/admin', roles: ROLE_USER_IP, ips: [127.0.0.1, ::1, '%env(TRUSTED_IPS)%'] }

        # для пользовательских потребностей сопоставления, используйте сервис сопоставителя запросов
        - { roles: ROLE_USER, request_matcher: App\Security\RequestMatcher\MyRequestMatcher }

        # требовать ROLE_ADMIN для маршрута 'admin'. Вы можете использовать сокращение "route: "xxx", вместо "attributes": ["_route": "xxx"]
        - { attributes: {'_route': 'admin'}, roles: ROLE_ADMIN }
        - { route: 'admin', roles: ROLE_ADMIN }

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

Пример #1:
  • URI /admin/user
  • IP: 127.0.0.1, Порт: 80, Хост: example.com, Метод: GET
  • Правило, которое применяется: правило #2 (ROLE_USER_IP)
  • Почему? URI совпадает с path, а IP - с ip.
Пример #2:
  • URI /admin/user
  • IP: 127.0.0.1, Порт: 80, Хост: symfony.com, Метод: GET
  • Правило, которое применяется: правило #2 (ROLE_USER_IP)
  • Почему? path и ip все еще совпадают. Это также будет совпадать с записью ROLE_USER_HOST, но используется только первое совпадение access_control.
Пример #3:
  • URI /admin/user
  • IP: 127.0.0.1, Порт: 8080, Хост: symfony.com, Метод: GET
  • Правило, которое применяется*: правило #1 (ROLE_USER_PORT)
  • Почему? path, ip и port совпадают.
Пример #4:
  • URI /admin/user
  • IP: 168.0.0.1, Порт: 80, Хост: symfony.com, Метод: GET
  • Правило, которое применяется: правило #3 (ROLE_USER_HOST)
  • Почему? ip не совпадает ни с первым, ни со вторым правилом.
  • Поэтому используется третье правило (которое совпадает).
Пример #5:
  • URI /admin/user
  • IP: 168.0.0.1, Порт: 80, Хост: symfony.com, Метод: POST
  • Правило, которое применяется: правило #3 (ROLE_USER_HOST)
  • Почему? Третье правило все еще совпадает. Это также будет совпадать с четвертым правилом
  • (ROLE_USER_METHOD), но используется только первое совпадение access_control.
Пример #6:
  • URI /admin/user
  • IP: 168.0.0.1, Порт: 80, Хост: example.com, Метод: POST
  • Правило, которое применяется: правило #4 (ROLE_USER_METHOD)
  • Почему? ip и host не совпадают с первыми тремя записями, но
  • четвертая - ROLE_USER_METHOD - совпадает и используется.
Пример #7:
  • URI /foo
  • IP: 127.0.0.1, Порт: 80, Хост: symfony.com, Метод: POST
  • Правило, которое применяется: не совпадает ни с одной записью
  • Почему? Не совпадает ни с одним правилом access_control, так как его URI
  • не совпадает ни с одним из значений path.

Caution

Сопоставление URI происходит без параметров $_GET. Откажите в доступе в PHP-кода , если вы хотите запретить доступ, основываясь на значениях параметра $_GET.

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

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

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

Tip

За кулисами, значение массива передается в качестве аргумента $attributes каждому избирателю в приложении с Request как $subject. Вы можете узнать, как использовать ваши пользовательские атрибуты, прочитав .

Caution

Если вы определите и roles, и allow_if, и вы используете Стратегию разрешения доступа по умолчанию (affirmative), то пользователю будет предоставлен доступ, если как минимум одно условие валидно. Если это поведение не подходит под ваши потребности, измениеть Стратегию разрешения доступа .

Tip

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

Сопоставление access_control по IP

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

Caution

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

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

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...
    access_control:
        #
        # опция 'ips' поддерживает IP-адреса и маски подсетей
        - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
        - { path: '^/internal', roles: 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:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...
    access_control:
        -
            path: ^/_internal/secure
            # опции 'roles' и 'allow_if' работают как выражение ОС, поэтому доступ
            # предоставляется, если выражение - TRUE, или если пользователь имеет ROLE_ADMIN
            roles: 'ROLE_ADMIN'
            allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"

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

Note

Внутренне, allow_if запускает встроенный ExpressionVoter, как будто бы он является частью атрибутов, определенных в опции roles.

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

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

Tip

Выражения allow_if могут также содержать пользовательский функции, зарегистрированные с помощью поставщиков выражений.

Ограничение по порту

Добавьте опцию port к любой записи access_control, чтобы потребовать от пользователей получение доступа к этим URL через конкретный порт. Это может быть ползено, к примеру, для localhost:8080.

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, port: 8080 }

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

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

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, requires_channel: https }