安全(Security)

Symfony的安全系统(security system)是非常强大的,但在设置它时也可能令人迷惑。在本大章中,你将学会如何一步步地设置程序的security,从配置防火墙(firewall)以及加载用户,到拒绝访问和取到用户对象。根据你的需求,有时在进行初始化设置时是痛苦的。然而一旦(配置)完成,Symfony的security系统在用起来时,则是既灵活又(希望能够)有乐趣。

由于有很多话要说,本章按几个大部头来组织:

  1. 初始化 security.yml 的设置 (authentication/验证 );

  2. 拒绝访问你的程序 (authorization/授权 );

  3. 获取当前的User对象;

它们被细分为许多小块内容(但仍然令人着迷),像是 logging out加密用户密码

1)初始化security.yml的设置 (Authentication/验证) 

security系统在 app/config/security.yml 中进行配置。默认的配置是这样的:

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

        default:
            anonymous: ~
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="default">
            <anonymous />
        </firewall>
    </config>
</srv:container>
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,
        ),
        'default' => array(
            'anonymous'  => null,
        ),
    ),
));

firewalls 键是security配置的 核心dev 防火墙并不重要,它只是确保Symfony开发工具 - 也就是居于 /_profiler/_wdt 之下的那些URL将不会被你的security所阻止。

你也可以针对请求中的其他细节来匹配一个请求(如 host主机)。阅读 如何把防火墙限制在特定请求 以了解更多内容和例程。

所有其他的URL将被 default 防火墙中处理(没有 pattern 键意味着它可以匹配所有 URL)。你可以认为防火墙就是你的security系统,因此通常只有一个主力防火墙就变得有意义了。但这并 意味着每一个URL都需要验证 - anonymous 键可以搞定这个。事实上,如果你现在去首页,是可以访问的,并将看到你以 anon. 身份“通过了验证”。不要被Authenticated旁边的“Yes”愚弄,你仍然只是个匿名用户:

security_anonymous_wdt

后面你将学习到如何拒绝访问特定URL或控制器(中的action)。

Security是高度 可配置的,在 Security配置参考 里展示了全部配置选项,并有一些附加说明。

A) 配置“如何令用户接受验证” 

防火墙的主要工作是去配置如何 让你的用户能够被验证。它们是否需要使用登录表单?或是HTTP basic验证?抑或一个API token?还是上面所有这些?

让我们从HTTP basic验证开始(老旧的弹出框)展开。要激活此功能,添加 http_basic 到firewall下面:

1
2
3
4
5
6
7
8
9
# app/config/security.yml
security:
    # ...

    firewalls:
        # ...
        default:
            anonymous: ~
            http_basic: ~
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="default">
            <anonymous />
            <http-basic />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'firewalls' => array(
        // ...
        'default' => array(
            'anonymous'  => null,
            'http_basic' => null,
        ),
    ),
));

真简单!要测试效果,你需要让用户登录并看到页面。为了让事情变的有趣,在 /admin 下创建一个新页面 。例如,如果你使用annotations,创建下例代码:

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

接下来,在 security.yml 中添加一个 access_control 入口,以令用户必先登录方可访问URL:

1
2
3
4
5
6
7
8
9
10
11
# app/config/security.yml
security:
    # ...
    firewalls:
        # ...
        default:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }
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="default">
            <!-- ... -->
        </firewall>
 
        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'firewalls' => array(
        // ...
        'default' => array(
            // ...
        ),
    ),
   'access_control' => array(
       // require ROLE_ADMIN for /admin*
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

你将学到关于 ROLE_ADMIN 以及后面的 2) 拒绝访问, Roles和其他授权 小节中的“拒绝访问”等更多内容。

太好了,如果你去访问 /admin,会看到HTTP Basic验证的弹出式登录:

security_http_basic_popup

但是,你是以何种身份登录进来?用户(信息)又来自哪里呢?

想要使用一个传统的form表单?很好!参考 如何建立一个传统的登录表单。还支持其他什么(验证)方式?参考 Configuration Reference 或者 构建你自己的

如果你的程序是通过诸如Google,Facebook或者Twitter等三方服务来完成登录,参阅 HWIOAuthBundle 社区bundle。

B) 配置“如何加载用户” 

当你在输入用户名时,Symfony就要从某个地方加载用户的信息。这就是所谓的“user provider”,由你来负责配置它。Symfony有一个内置方式来 从数据库中加载用户,但也可以 创建你自己的user provider

最简单的方式(但有很多限制),是配置Symfony从 security.yml 文件里直接加载写死在其中的用户。这被称为“in memory” provider,但把它观想为“in configuration” provider更合适些:

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'
    # ...
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>
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)之下,配置“要使用哪一个”provider。

参考 如何使用多个User Providers 来了解multiple providers设置的全部细节。

试着以用户名 admin 和密码 kitten 来登录。你应该看到一个错误!

No encoder has been configured for account "Symfony\Component\Security\Core\User\User"

要修复此问题 ,添加一个 encoders 健:

1
2
3
4
5
6
7
# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
    # ...
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>
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 providers加载用户信息,并将其置于一个 User 对象中。如果你从数据库中加载用户 或者 从其他来源加载,你需要使用自定义的User类。但当你使用“in memory” provider时,它直接给了你一个 Symfony\Component\Security\Core\User\User 对象。

无论你使用什么样的User类,你都需要去告诉Symfony它们的密码加密算法是什么。在本例中,密码只是明文的文本,但很快,你要把它改成 bcrypt

如果你现在刷新,你就会登录进来!web除错工具栏会告诉你,你是何人,你的roles(角色)是什么:

Symfony_loggedin_wdt

由于此URL需要的是 ROLE_ADMIN ,如果你登录的是 ryan 用户,将被拒绝访问。更多内容参考后面的 [URL受到保护的条件(access_control](#catalog9)。

从数据库加载用户 

如果你想通过Doctrine Orm加载用户,很容易!参考 如何从数据库中(Entity Provider)加载Security系统之用户 以了解全部细节。

C) 对用户密码进行加密 

不管用户是存储在 security.yml 里,数据库里还是其他地方,你都需要去加密其密码。堪用的最好算法是 bcrypt

1
2
3
4
5
6
7
8
# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost: 12
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>
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,
        )
    ),
    // ...
));

当然,用户密码现在需要被这个指定算法加密。对于写死(在config.yml中)的用户,你可以用内置命令来完成:

1
$  php bin/console security:encode-password

它将带给你下面这样(密码被加密)的东东:

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

现在一切都和以前一样。但如果你有动态用户(如,数据库中的),其密码在入库之前,你如何才能程序化的加密之呢?别担心,参考 手动加密密码 以了解细节。

此种方式的加密算法能否受到支持取决于你的PHP版本,但是包括 hash_algos PHP函数所返回的算法,以及一些其他(如 bcrypt)都受到支持。参考 Security参考 中的 encoders 选项键作为示例。

针对不同的用户,分别使用不同的算法也是可能的。参考 如何动态选择密码加密算法 以了解更多细节。

D) 配置完成! 

祝贺你!现在你有了一个可以使用的基于HTTP basic验证、并从 security.yml 文件中加载用户的“验证系统”了。

根据你的设置,尚有后续步骤:

2) 拒绝访问,Roles和其他授权方式 

现在,用户可以通过 http_basic 或者其他方式来登录你的程序。了不起呀!现在你需要学习如何拒绝访问(deny access)并且能与User对象一起工作。这被称为authorization(授权),它的工作是决定用户是否可以访问某些资源(如一个URL、一个model对象、一个被调用的方法等...)。

授权过程分为两个不同的方面:

  1. 用户在登录时收到一组特定的Roles(角色。如 ROLE_ADMIN)。

  2. 你添加代码,以便某个资源(例如url、控制器)需要一个特定“属性”(多数时候就是一个类似 ROLE_ADMIN 的role)才能被访问到。

除了roles(如 ROLE_ADMIN ),你还可以使用其他属性/字符串来(如 EDIT)来保护一个资源,并且通过使用voters或Symfony的ACL系统,来令它们生效。这在你需要检查用户A能否“编辑”对象B(如,id为5的产品)时极为好用。参考 Access Control Lists (ACLs):保护单个数据库对象

Roles/角色 

当有用户登录时,他们会接收到一组roles(如 ROLE_ADMIN)。在上例中,这些(roles)都被写死到 security.yml 中了。如果你从数据库中加载用户,它们应该存在了表的某一列中。

你要分给一个用户的所有roles,一定要以 ROLE_ 前缀开始。否则,它们不会被Symfony的Security系统按常规方式来操作(除非使用高级手段,否则你把 FOO 这样的role分给一个用户,然后像 下面这样 去检查 FOO 是行不通的)。

roles很简单,而且基本上都是你根据需要自行创造的字符串。例如,如果你打算限制访问你网站博客的admin部分,可以使用ROLE_BLOG_ADMIN 这个role来进行保护。role毋须在其他地方定义-你就可以开始用它了。

确保每个用户至少有一个 角色,否则他们会被当作未验证之人。常见的做法就是给每个 普通用户一个 ROLE_USER

你还可以指定role层级,此时,拥有某些roles意味着你同时拥有了其他的roles。

添加代码以拒绝访问 

要拒绝访问(deny access)某些东东,有两种方式可以实现:

通过条件匹配来保护URL(access_control) 

对程序的某一部分进行保护时的最基本方法是对URL进行完整的条件匹配。之前你已看到,任何匹配了正则表达式 ^/admin 的页面都需要 ROLE_ADMIN

1
2
3
4
5
6
7
8
9
10
11
12
# app/config/security.yml
security:
    # ...

    firewalls:
        # ...
        default:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }
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="default">
            <!-- ... -->
        </firewall>
 
        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN" />
    </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(
    // ...
 
    'firewalls' => array(
        // ...
        'default' => array(
            // ...
        ),
    ),
   'access_control' => array(
       // require ROLE_ADMIN for /admin*
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

能够保护全部(URL所属的)区域固然很好,但你可能还想 保护控制器的某一action

如果需要,你可以定义任意多个URL匹配条件 - 每个条件都是正则表达式。但是,仅仅有一个会被匹配。Symfony会从(文件中的)顶部开始寻找,一旦发现 access_control 中的某个入口与URL相匹配,就立即结束。

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

在path中加了一个 ^ 是指,仅当URL“按照正则条件那样起头”时,才会匹配到。例如,一个path若只有 /admin(不包含 ^)则会匹配到 /admin/foo 但同时也匹配了 /foo/admin 这种。

理解access_control是如何工作的

access_control 部分异常强大,但如果你不明白它的工作原理,它也会很危险(毕竟涉及到安全性)。 access_control 除了匹配URL,还可匹配IP地址、主机名和HTTP method。它也可以用于将用户重定向到 https 版本的URL条件中去。

要了解这一切,参考 Security的access_control是如何工作的

保护控制器和代码中的其他部分 

在控制器中你可以轻松谢绝访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
 
public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    // 第二个参数用于指定“要将role作用到什么对象之上”
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');
 
    // Old way / 老办法:
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Unable to access this page!');
    // }
 
    // ...
}

两种情况下,一个特殊的 AccessDeniedException 会被抛出,这最终触发了Symfony内部的一个403 HTTP响应。

就是这样!如果是尚未登录的用户,他们会被要求登录(如,重定向到登录页面)。如果他们已经 登录,但不具备 ROLE_ADMIN 角色,则会被显示403拒绝访问页面(此页可以 自定义)。如果他们已登录同时拥有正确的roles,代码就会继续执行。

多亏了SensioFrameworkExtraBundle,你可以在控制器中使用annotations

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 以了解更多。

模版中的访问控制 

如果你想在模版中检查当前用户是否具有一个role,可以使用内置的 is_granted() helper函数:

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

保护其他服务 

若像“保护控制器的”那样编写一些类似代码,Symfony中的任何地方都可以被保护。假设你有一个服务(即一个php类)用于发送电子邮件。你可以限制使用此类 - 不管它被用在何处 - 只有特定用户可以使用它。

参考 如何保护程序中的服务和方法 以了解多。

检查用户是否已登录(IS_AUTHENTICATED_FULLY) 

目前为止,你已经检查了基于role的访问 - 那些以 ROLE_ 前缀开头的字符串,被分配给了用户。但是,如果你 想检查用户是否登录(并不关心什么role不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();
    }
 
    // ...
}

你当然也可以使用在 access_control 中使用它。

IS_AUTHENTICATED_FULLY 不是一个role,但是它的某些行为又像是role,并且每个成功登录的用户都有这么一个。事实上,类似的特殊属性共有三个:

  • IS_AUTHENTICATED_REMEMBERED所有 已登录用户都有它,哪怕他们是通过“remember me cookie”登录进来的。就算你并没有使用 remember me功能,你依然能够通过这个属性来检查用户是否已经登录。

  • IS_AUTHENTICATED_FULLY:类似于 IS_AUTHENTICATED_REMEMBERED ,但更健壮。那些仅凭 “remember me cookie”中的 IS_AUTHENTICATED_REMEMBERED 登录进来的用户,并不会拥有 IS_AUTHENTICATED_FULLY

  • IS_AUTHENTICATED_ANONYMOUSLY所有 用户(甚至是匿名用户)都有此属性-当把URL置于白名单 以确保能被访问时,它很有用。参考 Security的access_control是如何工作的 以了解更多。

你还可以在模版中使用表达式:

1
2
3
4
5
{% if is_granted(expression(
    '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
)) %}
    <a href="...">Delete</a>
{% endif %}
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; ?>

关于表达式和security性的更多细节,参考 Security: 复杂的Access Controls表达式.

Access Control Lists (ACLs):保护单个数据库对象 

试想,你正在设计一个博客,用户可以在主题下面发表评论。你还想让用户能编辑自己的评论,但其他用户不要想。此外,作为管理员用户,你希望能够编辑所有 评论。

要做到这一点,你有两个选择:

  • Voters 允许用户编写自己的业务逻辑(如,用户可以编辑这篇文章是因为他们是创建人)来检查访问。你可能会使用这个选择 - 它足够灵活,可以解决以上问题。

  • ACLs 允许你创建一个数据库结构,在其中,你可以对任意 用户分配针对任意对象的任意 访问权限(如,EDIT、VIEW)。要使用ACLs,你需要一个管理员用户,以便通过某些管理界面,来对你的系统进行自定义的权限分配(grant custom access)。

两种情况下,你仍然需要使用与前面例子中相类似的方法来实施deny access(拒绝访问)。

获取用户对象 

验证之后,就可以通过 security.token_storage 服务来访问当前用户的 User 对象。在控制器内,这样写:

1
2
3
4
5
6
7
8
9
10
11
public function indexAction()
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }
 
    $user = $this->getUser();
 
    // the above is a shortcut for this / 上面的写法是通过以下取法(再进行授权)的快捷方式
    $user = $this->get('security.token_storage')->getToken()->getUser();
}

用户将是一个对象,该对象所属的类,则取决于你的user provider

3.2 通过方法签名(method signature)来取得用户的功能,是从Symfony 3.2开始引入的。如果你继承了 Controller,则仍然可以通过调用 $this->getUser() 来获取。

现在,基于你的 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 的类型提示并且登入(being logged in)是可选的时候,你可以为参数配置null值:

1
2
3
4
5
public function indexAction(UserInterface $user = null)
{
    // $user is null when not logged-in or anon.
    // 当用户没有登陆,或者是anno.时,$user是null
}

观点是这样的:在使用User对象之前应该始终检查用户是否已登录,可使用 isGranted 方法(或 access_control)来完成:

1
2
3
4
5
6
7
8
9
10
11
// yay! Use this to see if the user is logged in
// 耶!使用这个来查看用户是否已登陆
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw $this->createAccessDeniedException();
}
 
// boo :(. Never check for the User object to see if they're logged in
// 哄!:(. 切勿通过检查User对象来判断用户是否已登陆
if ($this->getUser()) {
 
}

在模版中获取用户 

对象在twig模版中可以使用app.user键:

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

Logging out/退出登录 

注意,当使用http-basic验证方式的防火墙时,是没有办法退出的:log out 的唯一方式就是令浏览器停止在每次请求中发送你的用户名和密码。清除浏览器缓存或重启它,往往有用。某些web开发工具(译注:可能是指浏览器的f12)也可能有用。

通常情况下,你希望用户能够注销。幸运的是,当你激活 logout 配置参数后,防火墙可以帮助你自动处理:

1
2
3
4
5
6
7
8
9
10
# app/config/security.yml
security:
    # ...

    firewalls:
        secured_area:
            # ...
            logout:
                path:   /logout
                target: /
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>
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创建一个路由(但不需要控制器):

1
2
3
# app/config/routing.yml
logout:
    path: /logout
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>
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)。

如果你需要在注销后做一些更有趣的事,可以通过添加 success_handler 键来指定一个“登出成功控制器”(logout success handler),将它填写成一个服务定义之id,该服务的class必须实现 LogoutSuccessHandlerInterface接口。参考 Security Configuration Reference

按等级划分的Roles 

并非把大量roles统统关联到用户,通过创建role hierarchy(角色层级),你可以定义一套“角色继承规则”:

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]
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>
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。ROLE_SUPER_ADMIN role,同时拥有 ROLE_ADMINROLE_ALLOWED_TO_SWITCH 以及 ROLE_USER(继承自 ROLE_ADMIN)。

总结 

喔~干得好!你已经了解到比security基础更多的内容。最难的部分就是当你有自定义需求时:像是定义一个验证策略(如,api tokens),复杂的授权逻辑以及许多其他事情(因为security本来就很复杂!)。

幸运的是:在这里有很多的文章,意在述清各种状况。同时,参考 Security参考。许多配置选项并无细节,然而当看到完整的配置树时,仍会有一定帮助。

祝好运!

了解更多 

Security Authentication (身份识别/登录用户) 

Security Authorization (拒绝访问) 

其他安全相关话题 

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

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