在symfony中,你可以使用ACL模块来检测用户访问数据的权限,但是他过于复杂。你可以使用一个更简单的方式就是自定义voter,他就像简单的条件语句。
看看 authorization 这一章可以对 voter 有更深刻的理解。
Symfony如何使用voter
为了使用voter,你应该了解symfony与voter的互动机制。所有的voter都要使用isGranted()方法调用,并在symfony接受授权检查(这个security.authorization_checker服务)。voter来决定用户是否可以访问这些资源。
根本上,symfony从所有的voter中获取响应,并根据应用程序的策略做最后的决定(允许或拒绝访问这些资源),策略主要有:affirmative, consensus和unanimous.
更多信息请查看the section about access decision managers.
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); } |
在symfony2.8新增了Voter这个辅助类。他和早期版本的AbstractVoter类类似。
设置:在控制器检查访问
假设你有一个post对象并且你需要决定当前用户是否可以编辑或是查看对象。在你的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 |
// 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 = ...; // check for "view" access: calls all 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 = ...; // check for "edit" access: calls all voters $this->denyAccessUnlessGranted('edit', $post); // ... } } |
这个denyAccessUnlessGranted()方法(以及,简单的isGranted()方法)来唤起“voter”系统。现在,没有voter能够决定用户是否可以访问或者编辑一个Post。但是你可以使用自己的逻辑去创建你自己的voter来决定你想要的。
denyAccessUnlessGranted()函数和这个isGranted()函数都是快捷方式他们都调用security.authorization_checker服务的
isGranted()
创建一个自定义的Voter
假设用普通的逻辑写法来决定用户是否可以访问或者编辑一个post对象是非常复杂的。例如,一个用户可以随时查看或者编辑他自己的post。或者这个post标记为公开,任何人都可以访问。但用一个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 |
// 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 if (!in_array($attribute, array(self::VIEW, self::EDIT))) { return false; } // only vote on Post objects inside this voter 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 /** @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 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 return $user === $post->getOwner(); } } |
就是这样完成了!接下来,我们来配置他;
回顾一下,这里是上面的两个抽象方法:
Voter::supports($attribute, $subject)
当isGranted()(或者denyAccessUnlessGranted())被调用的时候,他的首个参数是$attribute(如 ROLE_USER,edit),第二个参数(如果有的话)为$subject(如 null,一个Post对象)。你的工作就是确定你的voter的vote(票)的attribute/subject组合。如果你返回true,voteOnAttribute()将会被调用。否则,你的voter完成后,一些其他的voter会继续这个过程。在本例中,如果attribue是
view
或edit并且object是一个Post实例,那么
你将返回true。
voteOnAttribute($attribute, $subject, TokenInterface $token)
如果你从support()中返回true,这个方法就会被调用。你的工作很简单:返回true允许访问或者返回false拒绝访问。这个$token能够获取到当前的用户对象(如果有的话)。在本实例中,包含了所有复杂的业务逻辑,最终确定是否可以访问。
配置这个voter
去注入这个voter进入security,你一定要把他生成为一个服务并且标记他为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 |
完成了!现在,当你传入view/edit和一个Post对象给isGranted(),你的voter将被执行并能够控制访问了。
在voter中检查角色(Roles)
AccessDecisionManager是在2.8被引入的:在他之前使用会引起CircularReferenceException异常。在早期版本中,你一定要注入service_container,并且获取security.authorization_checker来使用isGranted().
如果你想从你的voter内调用isGranted() – 例如,你想看看当前用户是否有ROLE_SUPER_ADMIN角色。在这里你可以在你的voter中注入AccessDecisionManager。你可以这样使用它,例如,始终允许有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 |
// 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! if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) { return true; } // ... all the normal voter logic } } |
下一步,编辑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 } |
就是这样!调用AccessDecisionManager的decide()本质上和调用控制器或其他地方的isGranted()是一样的(他有点低级但是是voter所必须的)。
这个security.access.decision_manager是私有的。这意味着你不能直接从控制器访问:你只能将其注入到其他的服务中。那好吧:所有情况下都可以使用
security.authorization_checker除了voter。
更改这个Access Decision Strategy
通常情况下,在任何时间里只有一个voter将投票vote(其他的将“弃权”,意味着supports()将返回false)。但在理论上,你可以创建多个voters分别投票(vote)给一个action或者对象。比如说,你有一个 voter 用来检测一个站点的会员是否已经超过了 18 岁。
为了处理这样的情况,access decision manager将使用access decision strategy。你可以根据你的需求配置。这里有三种可用的策略:
affirmative (default)
一个voter授权访问时,给予授权。
consensus
当大多数的voter都允许访问时,给予授权。
unanimous
只有所有 voters 都允许授权的时候给予授权。
考虑上面的情况,也就是说我们所有的voter都允许访问才能允许授权,来读取post。在这种情况下,默认策略应该就不会在生效了,而是被unanimous所取代。你可以在安全配置里设置此参数:
1 2 3 4 |
# app/config/security.yml security: access_decision_manager: strategy: unanimous |