安全(Security)

symfony的安全系统是非常强大的,但它去设置时也可能会造成混淆。在本章中,您将学会如何一步一步的设置应用程序的安全性,从配置防火墙和你怎样拒绝这些用户进入和获取用户对象。根据你所需要的复杂程度,可能有时初始设置也很困难。但是,它一旦完成,symfony的安全系统是即灵活又可以(像你希望的一样)有乐趣的去工作。

由于有很多话要说,这一章主要分为以下几大内容:

1.开始设置security.yml(认证);

2.拒绝访问你的应用(授权);

3.获取当前用户对象;

其余还有很多小的部分(但仍然令人着迷),像注销编译用户密码

1)开始设置security.yml(认证) 

这个安全系统是配置在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键是你安全系统的心脏。这个dev防火墙并不重要,他只是确保symfony开发工具-像网址/_profiler/_wdt不会被你的安全策略所阻止。

你也可以匹配其他细节的请求(例如 host主机)。更多细节和案例请阅读 如何限定防火墙使其只允许通过指定请求

所有的其他URL都可以在default防火墙中处理(没有pattern键意味着它可以匹配所有URL)。你可以想象这个防火墙如同你的安全系统,因此它通常只有一个主要的防火墙是有意义的。但这并不意味着每一个URL都要求验证身份-该anonymous键重点用于完成这个功能。事实上,如果你现在直接访问首页,你可以访问成功并且你会看到你的“authenticated”为anon.。不要被Authenticated旁边的“Yes”愚弄,你只是一个匿名用户:

security_anonymous_wdt

稍后您将学习如何拒绝用户访问某些URL或控制器。

Security是高度可配置的,并有一个安全配置手册,这个手册显示所有的选项和一些额外的解释。

A)如何让你的用户进行身份验证的配置 

防火墙的主要工作是如何配置你的用户进行身份验证。他们将使用一个登录表单?基础的Http?一个api令牌?所有上面的方式?

让我们从基本的http工作开始(老旧的http弹出方式)。想要激活此功能,需要添加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。例如,如果你使用注释方式,应该这样创建:

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_controlsecurity.yml需要这个用户访问网址时,先登录。

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的东西和后面会遇到的拒绝访问方式,在后面的拒绝访问,角色和其他授权章节

太好了,你现在就去访问 /admin ,你会看到HTTP Basic方式弹出登录:

security_http_basic_popup

但是谁能够登录?用户从哪里来呢?

你要使用一个传统的form表单吗?太好了!可以看看 如何建立一个传统的登录表单。还支持什么方法?请查看 Configuration Reference或者建设我们自己的

如果你的应用程序登录是通过第三方服务例如Google,Facebook或者Twitter,那么就查看 HWIOAuthBundle 社区bundle

B)怎么样配置用户 

当你输入你的用户名,symfony就需要加载用户的信息。这就是所谓的“user provider”,而你要配置他。symfony有一个内置的方式可以从数据库加载用户,还可以创建自己的user provider

我们使用最简单的方式(但这种方式有很多限制),这种配置是在security.yml里用硬编码的方式来加载用户。这就是所谓的“in memory” 提供器(provider),但最好是把它作为一个“in configuration”提供器:

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来使用(例如provider:in_memory)。

想了解更多多个提供器的设置细节请阅读:如何使用多个User 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对象。如果你从数据库加载用户(load users from the database)或者使用一些其他的方式(some other source),你需要使用你自己定义的User类。当你使用“in memory” provider时,你就要给他一个Symfony\Component\Security\Core\User\User对象。

无论你使用什么用户类,你都需要去告诉symfony他们的密码的编码算法是什么。在本例中,这个密码使用的是明文的,但在第二个例子中,你需要改变它成bcrypt

如果你现在刷新,你就会登录进来!网页的调试工具栏会告诉你,你是谁你的角色是什么:

symfony_loggedin_wdt

由于此URL需要ROLE_ADMIN,如果你登录的是ryan,这会拒绝你的访问。后面还有(URL保护模式(ACCESS_CONTROL))。

从数据库加载用户 

如果你想通过doctrine orm加载你的用户这很容易!请参阅 怎样从数据库加载安全用户(the Entity Provider)

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

当然,你的用户密码现在需要准确的算法加密。对于硬编码用户,你能够使用一个内置的命令行:

$ 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函数以及一些其他方法(例如bcrypt)算法才会正确返回。可以看看Security Reference Section中案例的encoders键。

还可以针对不同的用户,分别使用不同的算法。详细内容请查看 如何动态选择密码加密算法

D)配置完成! 

恭喜!现在您可以在security.yml文件使用http Basic和中加载用户的方式来让认证系统工作了。

你的下一步需要你来设置:

2)拒绝访问,角色和其他授权 

现在,用户可以通过http_basic或者其他方式来登录你的应用程序。太好了!现在你需要学习如何拒绝访问并且能够与用户对象一起工作。这就要所谓的授权,他的工作是决定一个用户是否能够访问一些资源(如一个URL、一个model对象、方法调用等)。

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

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

  2. 你添加的资源(例如url、控制器)需要特定“属性”(例如一个ROLE_ADMIN角色)才能够访问它。

除了角色(例如ROLE_ADMIN),你还可以使用一些其他的属性/字符串来(例如 EDIT)保护资源或者使用voters和symfony的ACL系统去做同样意义的工作。这可能会派上用场,如果你需要检测用户A能否编辑对象B(例如,id为5的产品)。更多信息请参见后面的:Access Control Lists (ACLs):保护单个数据库对象

角色 

当一个用户登录,他会接收到一个设置好的角色(例如 ROLE_ADMIN)。在上面的例子中,这些都被硬编码到security.yml中。如果你正在数据库中加载用户,这些都应该存储在你表的列中。

所有角色你要分给一个用户一定要以ROLE_前缀为开始。否则,他们不会被symfony的安全系统认定为正常的方式(除非你正在做一些先进的事,分配一个角色FOO给一个用户,然后检查FOO在下面的例子中是行不通的)。

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

确保每个用户最少有一个角色,或者你的用户不需要身份验证。常见的做法就是给普通用户一个ROLE_USER角色。

你还可以设置角色的层级结构,symfony会自动知道你的其他角色。

添加代码以拒绝访问 

有两种方式可以实现拒绝访问:

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模式-每一个都是正则表达式。但是,仅仅有一个会匹配。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'),
    ),
));

路径使用^的意识是指定URL开始的地方去进行模式匹配。例如,一个简单的路径为/admin(不包含^)将匹配/admin/foo也将匹配/foo/admin

了解access_control如何工作

这个access_control部分非常强大,但是如果你不明白他的工作原理,他也很危险(因为毕竟它涉及到安全性问题)。access_control除了匹配URL还可匹配IP地址、主机名和http方法。他也可以将用户重定向到https的URL模式上。

想了解这一切,你可以看看 安全的access_control是如何工作的?.

保护控制器和其他地方 

您还可以轻松拒绝来自控制器的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
 
public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    $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拒绝访问页面(可以自定义)。如果它们登录并拥有正确的角色,该代码就会被执行。

多亏了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.

模版中的访问控制 

如果你想在模版中检测当前用户权限,可以使用内置的is_granted()辅助函数:

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_前缀并非配给用户。但是,如果你只是想检测用户是否登录。(不关心角色)那么你可以使用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不是一个角色,但是他的这种行为又像是一个角色,并且每个用户成功登录后都要有。事实上,有三个这样的特殊属性:

  • 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访问时他很有用。更多细节请查看 安全的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: 复杂的Access Controls表达式.

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

想象一下,你正在设计一个博客,用户可以在自己的帖子里发表评论。您还希望用户能够编辑自己的评论,不用别的用户编辑。此外,作为管理员用户,您希望能够编辑所有评论。

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

  • Voters 允许用户使用业务逻辑(例如 用户可以编辑这篇文章,因为他们的创造者)去查明访问。可能会需要这个配置-他足够灵活,可以解决以上问题。

  • ACLs 允许你在系统中创建一个数据库结构,你可以给不同的用户分配不同的访问(例如 EDIT、VIEW)组成任意对象。你可以在你系统的管理界面使用管理员用户分配自定义访问。

在这两种情况下,你仍然要配合上面的拒绝访问方法来使用。

获取用户对象 

认证之后,访问当前用户的User对象就需要调用security.token_storage服务。在控制器内,这样写:

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

现在你可以在你的对象中调用任何方法。例如,调用一个用户对象的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

问题是这样的:你应该在使用用户前,经常检查用户是否登录并使用isGranted方法或者access_control去做:

1
2
3
4
5
6
7
8
9
// 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
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; ?>

注销 

通常情况下,你希望用户能够注销。幸运的是,当您激活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键去添加一个登录成功后的处理程序这个将指向一个有LogoutSuccessHandlerInterface接口的服务类的id。详情请查看 Security Configuration Reference.

注意当使用http-basic做防火墙认证时,没有真正的退出方式:注销的唯一方法是让浏览器在每次请求停止发送你的名字和密码。清除你的浏览器缓存或重启浏览器通常会有帮助。一些web开发工具可能会帮到你。

角色分级 

很多角色的用户都是关联的,你可以创建一个角色的分级来定义角色的继承关系:

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_SUPER_ADMIN角色有ROLE_ADMINROLE_ALLOWED_TO_SWITCHROLE_USER(继承自ROLE_ADMIN)。

无状态认证 

默认情况下,Symfony依赖cookie(会话session)去持久化用户的安全上下文。但如果你使用证书或HTTP认证,持久化不需要每个请求都有证书可用。在那种情况下,如果不需要在两个请求之间保存什么的话,你可以激活无状态认证(意思是没有cookie被Symfony创建):

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

    firewalls:
        main:
            http_basic: ~
            stateless:  true
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>
        <!-- ... -->
 
        <firewall name="main" stateless="true">
            <http-basic />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'firewalls' => array(
        'main' => array('http_basic' => null, 'stateless' => true),
    ),
));

如果你使用表单登录的话,即使你将stateless设置true,Symfony也将创建一个cookie。

总结 

哇,很不错!你现在知道了很多安全的基础知识。最难的部分就是你需要定制的时候:像是定义一个认证策略(例如 api tokens),复杂的授权逻辑或者许多其他的事情(因为安全本来就很复杂!)。

幸运的是:在这里有很多的文章,它描述了多种情况。此外,还可以参考Security Reference。许多配置没有具体的细节介绍,但是看到完整的配置树我想他可能很有帮助。

祝你好运;

了解更多 

安全认证 Security Authentication (Identifying/Logging in the User) 

安全授权 Security Authorization (Denying Access) 

其他安全相关的话题 

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

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