Security Voters提供了一种架构,用于在Symfony程序中设置细粒化的访问限制。相对于ACL,其主要优势在于设置和使用的难度低了一个数量级。

之前的Symfony版本中,voter实现的是VoterInterface接口,具有下述特征:

1
2
3
4
5
6
interface VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
}

实现这个接口并不难,但却导致了代码过度膨胀,举例来说,下面83行代码仅仅为了定义一个简单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
79
80
81
82
83
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;
 
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
 
class PostVoter implements VoterInterface
{
    const VIEW = 'view';
    const EDIT = 'edit';
 
    public function supportsAttribute($attribute)
    {
        return in_array($attribute, array(
            self::VIEW,
            self::EDIT,
        ));
    }
 
    public function supportsClass($class)
    {
        $supportedClass = 'Acme\DemoBundle\Entity\Post';
 
        return $supportedClass === $class || is_subclass_of($class, $supportedClass);
    }
 
    /**
     * @var \Acme\DemoBundle\Entity\Post $post
     */
    public function vote(TokenInterface $token, $post, array $attributes)
    {
        // check if class of this object is supported by this voter 检查对象的类是否被voter支持
        if (!$this->supportsClass(get_class($post))) {
            return VoterInterface::ACCESS_ABSTAIN;
        }
 
        // check if the voter is used correct, only allow one attribute 检查voter的正确使用,只允许1个属性
        // this isn't a requirement, it's just one easy way for you to 
        // design your voter 这并非必须,只是个易用方式,供你打造voter时参考
        if(1 !== count($attributes)) {
            throw new \InvalidArgumentException(
                'Only one attribute is allowed for VIEW or EDIT'
            );
        }
 
        // set the attribute to check against 设置待检属性
        $attribute = $attributes[0];
 
        // check if the given attribute is covered by this voter 检查给定属性是否被本voter覆盖
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }
 
        // get current logged in user 取得user信息
        $user = $token->getUser();
 
        // make sure there is a user object (i.e. that the user is logged in) 确保是user对象
        if (!$user instanceof UserInterface) {
            return VoterInterface::ACCESS_DENIED;
        }
 
        switch($attribute) {
            case self::VIEW:
                // the data object could have for example a method isPrivate() 假定数据对象有一个isPrivate()方法
                // which checks the Boolean attribute $private 用于检查布尔值的属笥$private
                if (!$post->isPrivate()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;
 
            case self::EDIT:
                // we assume that our data object has a method getOwner() to 假设数据对象有个getOwner()方法
                // get the current owner user entity for this data object 得到当前数据对象的拥有者entity
                if ($user->getId() === $post->getOwner()->getId()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;
        }
 
        return VoterInterface::ACCESS_DENIED;
    }
}

根据DX开发体验的讨论结果,Symfony 2.6将允许定义简化security voters。为了实现简化效果,请使用全新AbstractVoter抽象类,它实现了VoterInterface接口,并定义了如下方法:

1
2
3
4
5
6
7
8
9
abstract class AbstractVoter implements VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
    abstract protected function getSupportedClasses();
    abstract protected function getSupportedAttributes();
    abstract protected function isGranted($attribute, $object, $user = null);
}

supportsAttribute()supportsClass()vote()三个方法,帮你减少了voter的样板式代码,令你集中火力到程序的逻辑上。因此,上面的voter现在只需41行代码即可实现:

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
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;
 
use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;
 
class PostVoter extends AbstractVoter
{
    const VIEW = 'view';
    const EDIT = 'edit';
 
    protected function getSupportedAttributes()
    {
        return array(self::VIEW, self::EDIT);
    }
 
    protected function getSupportedClasses()
    {
        return array('Acme\DemoBundle\Entity\Post');
    }
 
    protected function isGranted($attribute, $post, $user = null)
    {
        // make sure there is a user object (i.e. that the user is logged in)
        if (!$user instanceof UserInterface) { //确保是User对象(该用户已经登陆)
            return false;
        }
 
        // custom business logic to decide if the given user can view
        // and/or edit the given post 自定义商务逻辑来决定当前用户是否能看到/编辑内容
        if ($attribute == self::VIEW && !$post->isPrivate()) {
            return true;
        }
 
        if ($attribute == self::EDIT && $user->getId() === $post->getOwner()->getId()) {
            return true;
        }
 
        return false;
    }
}

少写了代码,却得到相同效果,于是推动了你的生产力。正因为DX开发体验的引入,这将是我们永不放弃的追求。Symfony 2.6是首个强调此哲学的版本。