如何使用Voter检查用户权限

3.4 版本
维护中的版本

在Symfony中,你可以使用 ACL模块 来检查用户在访问数据时的权限,但对许多程序来说它过于复杂。一个容易得多的方案是使用自定义voter,它就像简单的条件声明。

阅读 authorization 章节以便对 voter 有更深的理解。

Symfony如何使用voter 

为了使用voter,你需要理解Symfony与是如何与之一起工作的。所有的voter都在你使用Symfony的authorization checker(即 security.authorization_checker 服务)的 isGranted() 方法时被调用。每一个(voter)决定当前用户是否可以访问某些资源。

基本上,Symfony从全部voter中获取响应,并根据程序(级别配置文件)所定义的策略(strategy)来做出最终决定(允许或拒绝访问相应的资源),策略可以是:affirmative, consensus和unanimous。

参考 Access Decision Manager 章节以了解更多。

Voter Interface 

自定义voter需要实现 VoterInterface 接口或者继承 Voter,继承方式令voter的创建更加容易。

1
2
3
4
5
abstract class Voter implements VoterInterface
{
    abstract protected function supports($attribute, $subject);
    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

设置:在控制器中检查访问权限 

假设你有一个 post 对象,你需要决定当前用户是否可以编辑(edit)或是查看(view)对象。在控制器(Controller)中,你可以使用下例代码来检查访问权限:

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
26
27
28
29
30
31
32
33
34
35
36
37
// src/AppBundle/Controller/PostController.php
// ...
 
class PostController extends Controller
{
    /**
     * @Route("/posts/{id}", name="post_show")
     */
    public function showAction($id)
    {
        // get a Post object - e.g. query for it
        // 获取 Post 对象 - 比如通过查询取得
        $post = ...;
 
        // check for "view" access: calls all voters
        // 检查 "view" 访问权限:调用全部voters
        $this->denyAccessUnlessGranted('view', $post);
 
        // ...
    }
 
    /**
     * @Route("/posts/{id}/edit", name="post_edit")
     */
    public function editAction($id)
    {
        // get a Post object - e.g. query for it
        // 获取 Post 对象 - 比如通过查询取得
        $post = ...;
 
        // check for "edit" access: calls all voters
        // 检查 "edit" 访问权限:调用全部voters
        $this->denyAccessUnlessGranted('edit', $post);
 
        // ...
    }
}

denyAccessUnlessGranted() 方法(以及,更简单的 isGranted() 方法)能够调动 “voter” 系统。此刻,并没有voter对用户“是否可以查看或者编辑一个 Post”进行表决(vote)。但是你可以创建自己的 voter,随需使用逻辑来决断之。

denyAccessUnlessGranted()isGranted() 函数都是 controller 类调用 security.authorization_checker 服务的 isGranted() 时的快捷方法。

创建自定义Voter 

假设,用于决定一位用户是否可以“查看”或者“编辑”一个post对象的逻辑是非常复杂的。例如,一个 User 可以随时查看或者编辑他们自己创建的 post。而如果 post 被标记为“public”,则任何人都可以查看。此时若使用voter则是下面这样:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// src/AppBundle/Security/PostVoter.php
namespace AppBundle\Security;
 
use AppBundle\Entity\Post;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 
class PostVoter extends Voter
{
    // these strings are just invented: you can use anything
    // 这些字符串都是随便起的:你可以使用任何字符
    const VIEW = 'view';
    const EDIT = 'edit';
 
    protected function supports($attribute, $subject)
    {
        // if the attribute isn't one we support, return false
        // 如果属性并非我们所支持的,返回 false
        if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
            return false;
        }
 
        // only vote on Post objects inside this voter
        // 在这个voter中,只对 Post 对象进行表决
        if (!$subject instanceof Post) {
            return false;
        }
 
        return true;
    }
 
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
 
        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            // 用户必须已登录;否则,拒绝访问
            return false;
        }
 
        // you know $subject is a Post object, thanks to supports
        // 多亏了supports,你知道 $subject 是 Post 对象
        /** @var Post $post */
        $post = $subject;
 
        switch ($attribute) {
            case self::VIEW:
                return $this->canView($post, $user);
            case self::EDIT:
                return $this->canEdit($post, $user);
        }
 
        throw new \LogicException('This code should not be reached!');
    }
 
    private function canView(Post $post, User $user)
    {
        // if they can edit, they can view / 若用户能编辑,表明其亦可查看
        if ($this->canEdit($post, $user)) {
            return true;
        }
 
        // the Post object could have, for example, a method isPrivate()
        // that checks a boolean $private property
        // Post 对象可以拥有,例如,一个用于检查布尔值 $private 属性的 isPrivate() 方法
        return !$post->isPrivate();
    }
 
    private function canEdit(Post $post, User $user)
    {
        // this assumes that the data object has a getOwner() method
        // to get the entity of the user who owns this data object
        // 这里假设数据对象中有一个 getOwner() 方法用于获取该对象拥有者的 User entity
        return $user === $post->getOwner();
    }
}

就是这样!接下来,配置它

回顾一下,以下是上例中两个抽象方法所要做的:

Voter::supports($attribute, $subject)
isGranted() (或 denyAccessUnlessGranted()) 被调用时,第一个参数作为 $attribute (如 ROLE_USER, edit) 传入,而第二个参数 (如果有) 则作为 $subject (如 null, 一个 Post 对象) 传入。你要做的,是决定你的voter是否应对 attribute/subject 组合进行表决。如果你返回true,voteOnAttribute() 将被调用。否则,你的voter就结束:其他的voter会处理这个。本例中,如果attribue是 viewedit,同时对象是 Post 实例的话,你要返回 true
voteOnAttribute($attribute, $subject, TokenInterface $token)
如果你从 supports() 中返回 true,那么本方法会被调用。你要做的很简单:返回 true 以允许访问,返回 false 拒绝访问。$token 可用于找到当前的user对象 (如果有)。本例中,所有复杂的业务逻辑被包容至此以决定访问权限。

配置Voter 

要把voter注入到security layer(译注:指框架的安全体系),你要将其声明为服务并打上 security.voter 标签:

1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.post_voter:
        class: AppBundle\Security\PostVoter
        tags:
            - { name: security.voter }
        # small performance boost
        public: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="app.post_voter"
            class="AppBundle\Security\PostVoter"
            public="false"
        >
 
            <tag name="security.voter" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$container->register('app.post_voter', 'AppBundle\Security\PostVoter')
    ->setPublic(false)
    ->addTag('security.voter')
;

妥了!现在,当你 调用isGranted()时传入view/edit和一个Post对象,此voter就会执行,你可以控制访问了。

在voter中检查Roles 

如果你想从voter内部 调用 isGranted() – 如,你想看看当前用户是否有 ROLE_SUPER_ADMIN。这是可能的,将 AccessDecisionManager 注入到voter即可。这样用的话你可以,例如,始终 允许拥有 ROLE_SUPER_ADMIN 的用户进行访问:

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
26
27
28
29
// src/AppBundle/Security/PostVoter.php
 
// ...
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
 
class PostVoter extends Voter
{
    // ...
 
    private $decisionManager;
 
    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }
 
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // ...
 
        // ROLE_SUPER_ADMIN can do anything! The power!
        // ROLE_SUPER_ADMIN 可以做任何事!威力大!
        if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
            return true;
        }
 
        // ... all the normal voter logic / 所有常规的voter逻辑
    }
}

接下来,更新 services.yml 以注入 security.access.decision_manager服务:

1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.post_voter:
        class: AppBundle\Security\PostVoter
        arguments: ['@security.access.decision_manager']
        public: false
        tags:
            - { name: security.voter }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="app.post_voter"
            class="AppBundle\Security\PostVoter"
            public="false"
        >
            <argument type="service" id="security.access.decision_manager"/>
 
            <tag name="security.voter" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->register('app.post_voter', 'AppBundle\Security\PostVoter')
    ->addArgument(new Reference('security.access.decision_manager'))
    ->setPublic(false)
    ->addTag('security.voter')
;

完成!调用 AccessDecisionManagerdecide() 本质上和调用控制器或其他地方的 isGranted() 是一样的(只是有些偏底层化[lower-level],对于voter来说是所必须的)。

security.access.decision_manager 是私有的(private)。这意味着你不能从控制器中直接访问:你只能反它注入到别的服务中。这样就好:在voter以外的所有使用场合,使用 security.authorization_checker 来替代之。

变更Access Decision策略 

一般来说,在任何给定时间只有一个voter将进行表决(vote)(其余的将“abstain/弃权”,这表明它们将从 supports() 中返回 false)。但是理论上,你可以创建多个voter来对一个action和对象进行表决。例如,假设你有一个voter用于检查用户是否是网站会员,而另外一个则检查用户是否大于18岁。

为了处理这类情形,access decision manager使用的是一个“access decision策略”。你可以随需配置它。有三种可用策略:

affirmative (默认)
此策略当有 一个 voter给予了访问权限时,即授权通过;
consensus
此策略当多数voters给予了访问权限时,即授权通过;
unanimous
此策略仅当 全部 voters都给予访问权限时,才授权通过。

在上面的场合中,两个voter都允许访问才能授权用户读取post主题。本例中,默认策略无法满足需求,应当用 unanimous 取代之。你可以在security配置中设置此选项:

1
2
3
4
# app/config/security.yml
security:
    access_decision_manager:
        strategy: unanimous
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:srv="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
>
 
    <config>
        <access-decision-manager strategy="unanimous">
    </config>
</srv:container>
1
2
3
4
5
6
// app/config/security.php
$container->loadFromExtension('security', array(
    'access_decision_manager' => array(
        'strategy' => 'unanimous',
    ),
));

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

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