Security的access_control是如何工作的?

3.4 版本
维护中的版本

对于每个到来的请求,Symfony都会逐个检查 access_control 条目(entry),以找到 一个 匹配当前请求的。在找到匹配的access_control 条目后,它马上就停止了 - 只有 第一个匹配的 access_control 用于执行访问。

每个 access_control 都有若干选项,用于配置两个不同的部分:

  1. 到来的请求应当匹配此次的访问控制之条目

  2. 一旦匹配,某种形式的访问限制应当被执行

1. 匹配规则的选项 

Symfony为每个 access_control 条目创建了 RequestMatcher 实例,以决定是否将给定的访问控制(access control)用于本次请求。以下是用于匹配的 access_control 选项:

  • path
  • ipips
  • host
  • methods

将下面的 access_control 条目,作为一个示例:

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 }
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>
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将基于URI、客户端IP地址、传入的主机名或请求方法(request method),来决定使用哪个 access_control。记得,第一个被匹配到的规则(rule)将被使用,并且,如果 ip, hostmethod 没在条目中没有被指定的话,access_control 将匹配任意的ip, hostmethod

URI IP HOST METHOD access_control Why? / 说明
/admin/user 127.0.0.1 example.com GET rule #1 (ROLE_USER_IP) URI 匹配 path 并且 IP 匹配 ip
/admin/user 127.0.0.1 symfony.com GET rule #1 (ROLE_USER_IP) pathip 仍然匹配。同时还匹配 ROLE_USER_HOST 条目,但 只有 第一个access_control 匹配的才会被使用。
/admin/user 168.0.0.1 symfony.com GET rule #2 (ROLE_USER_HOST) ip 不匹配第一个规则 , 因此第二个规则(如果匹配的话)会被使用。
/admin/user 168.0.0.1 symfony.com POST rule #2 (ROLE_USER_HOST) 第二个规则仍然匹配。 第三个规则也能匹配 (ROLE_USER_METHOD), 但只有 第一个 匹配的 access_control 才会被使用。
/admin/user 168.0.0.1 example.com POST rule #3 (ROLE_USER_METHOD) iphost 不能匹配到前两项, 但第三项 - ROLE_USER_METHOD - 能够匹配,并被使用。
/admin/user 168.0.0.1 example.com GET rule #4 (ROLE_USER) The ip, hostmethod 阻止了前三个规则被匹配。但由于 path 条件能够匹配ROLE_USER条目中的URI,它会被使用。
/foo 127.0.0.1 symfony.com POST matches no entries 它不能匹配任何 access_control 规则, 因为它的URL不能匹配任何一个 path 的值。

2. 访问控制的实施 

一旦 Symfony 决定了哪些 access_control 条目能够匹配 (如果有的话),那么它将 执行 基于 roles, allow_ifrequires_channel 选项的访问限制:

  • role 如果用户不持有给定的role(s),则访问被拒绝(在内部,会抛出一个 AccessDeniedException 异常);

  • allow_if 如果表达式返回 false ,则访问被拒绝;

  • requires_channel 如果到来的请求通道 (如 http) 无法匹配这个值 (如 https),用户会被重定向(如,从 http 重定向到 https,反之亦然)。

如果访问被拒绝,若是未经认证的用户,系统将尝试去认证他(如,将他重定向到登录页面)。如果用户已经登录,将会显示403 “access denied” 的错误页面。参考 如何自定义错误页

通过IP来匹配access_control 

当你需要一个 access_control 条目,而它 只能 去匹配某些IP地址和IP范围的请求时,则可能面临特定情形。例如,除了 受到信任的内部服务器,其他所有的URL条件(pattern)的请求,都被拒绝访问。

正如你在下例中看到的解释,ips 选项不能限制一个特定的IP地址。相反,使用 ips 键意味着 access_control 条目将仅匹配IP地址,而用户从不同ip地址访问它时,将被 access_control list 阻拦。

这里是关于你如何配置一些 /internal* URL 模式例子的示例,所以它只能从本地服务器的请求来访问:

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 }
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>
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'
        ),
    ),
));

当外部IP地址 10.0.0.1 进入 /internal/something 路径(path)时,下面是其工作过程:

  • 第一个访问控制规则(access control rule)将被忽略,因为 path 能匹配,但IP地址没有匹配到IP列表;

  • 第二个访问控制规则将被启用(这里只限制了 path),所以它匹配了。如果你确保没有用户持有 ROLE_NO_ACCESS,则访问会被拒绝(ROLE_NO_ACCESS 可以是“不匹配现有role”的任何内容,它作为一种技巧而存在,用于拒绝所有访问)。

但如果相同的请求来自 127.0.0.1 或者 ::1 (IPV6 loopback address):

  • 现在,第一个访问控制规则被启用,因为 pathip 都能够匹配:因为用户总是持有 IS_AUTHENTICATED_ANONYMOUSLY 这个role,所以可以访问。

  • 第二个访问规则就无法通过了,因为第一个规则已经匹配。

通过表达式来保护 

一旦有一个 access_control 条目被匹配, 你可以通过 roles 键,或者使用 allow_if 键下面的表达式,以形成更复杂逻辑,来拒绝访问:

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')"
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>
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")',
    ),
),

这种情况下,当用户试图访问任何以 /_internal/secure 开头的网址时,如果 IP 地址是 127.0.0.1,或者用户拥有 ROLE_ADMIN 这个role,他们将被授权访问。

在表达式内部,你可以访问许多变量和函数,包括 request, 它正是Symfony的 Request 对象 (见 Request)。

关于更多函数和变量,参考 函数和变量

强制使用一种通道(http,https) 

你也可以要求用户通过SSL访问URL;只需在任意 access_control 条目中使用 requires_channel 参数即可。如果 access_control 被匹配,并且请求(request)使用的是 http 通道,那么用户将被重定向到 https

1
2
3
4
5
# app/config/security.yml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
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>
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',
        ),
    ),
));

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)