如何使用 Voter 检查用户权限
在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是view
或edit
,同时对象是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')
; |
完成!调用 AccessDecisionManager
的 decide()
本质上和调用控制器或其他地方的 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',
),
)); |