如何使用Access Control Lists (ACLs)

3.4 版本
维护中的版本

在复杂的程序中,你经常会面临一个问题,即访问权限之决策(access decisions)并非仅取决于“请求访问者”本人(Token),而是还牵涉到“被请求访问的域对象”。这正是 ACLs 系统诞生之由头。

ACL的替代选择

使用ACL非同小可,对于简单场合,它存在过度杀伤之嫌。如果你的权限逻辑(permission logic)可以通过编写某些代码来表述(如,检查一条博客主题是否为当前用户所有),应考虑使用 voter。Voter接收那个要被表决的对象,因此你可通过该对象来完成复杂决策,并有效实施自己的ACL。授权检查(enforcing authorization,即 isGranted 部分)看上去和本文所述相类似,但是在幕后处理(决策)逻辑的是你的 voter 类,而不是借助 ACL 系统。

假设你正在设计一个博客系统,用户可以对你的主题进行评论。现在,你希望用户能够编辑自己的评论,而不是其他用户的;此外,你自己能够编辑所有人的评论。在这种场合,Comment 就是域对象(domain object。译注:Symfony中的entity都可算作域对象),你要对它限制访问。使用Symfony你有若干种方式可以做到这一点,两个基本方式是(非具体描述):

  • 在逻辑方法中执行安全检查 :基本上,这意味着,对所有能够访问的用户,在每个 Comment 中保留一个(对这些用户的)引用,然后,针对所提供的Token对象来比对这些用户。

  • 使用Role来执行安全检查 :这种方式,为每个 Comment 对象添加角色,如 ROLE_COMMENT_1ROLE_COMMENT_2 等。

两种方式都可以完美实现。然而,这会把授权逻辑(authorization logic)和业务逻辑代码(business code)耦合起来,导致难以复用,并增加了单元测试的难度。此外,如果很多用户访问同一个域对象,你的代码可能会遇到性能问题。

幸运的是,有一种更好的方式,你即将看到。

启动 

现在,在最终实战之前,你需要做一些启动准备。首先,你要配置ACL系统会用到的连接(connection):

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

    acl:
        connection: default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 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>
        <!-- ... -->
 
        <acl connection="default" />
    </config>
</srv:container>
1
2
3
4
5
6
// app/config/security.php
$container->loadFromExtension('security', 'acl', array(
    // ...
 
    'connection' => 'default',
));

ACL系统必须有一个connection,connection可以是Doctrine DBAL(默认使用)也可以是Doctrine MongoDB(用于 MongoDBAclBundle)。然而,这并不代表你必须用Doctrine ORM或者ODM来映射你的域对象。你可以用你喜欢的任何mapper来映射对象,比如 DoctrineORM,MongoDB ODM,Propel,原生SQL 等等。由你来决定。

在配置了连接(connection)之后,你还要执行下述命令来导入数据结构:

1
$  php bin/console init:acl

入门 

回到一开始的小例子,你现在可以用ACL实现它了。

一旦创建了ACL,你就可以通过创建一个Access Control Entry(ACE)来赋予(域)对象一个访问权限,以强化entity和你的用户之间的关系。

创建一个ACL并添加一个ACE 

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/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
 
class BlogController extends Controller
{
    // ...
 
    public function addCommentAction(Post $post)
    {
        $comment = new Comment();
 
        // ... setup $form, and submit data / 设置 $form,并提交数据
 
        if ($form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($comment);
            $entityManager->flush();
 
            // creating the ACL / 创建 ACL
            $aclProvider = $this->get('security.acl.provider');
            $objectIdentity = ObjectIdentity::fromDomainObject($comment);
            $acl = $aclProvider->createAcl($objectIdentity);
 
            // retrieving the security identity of the currently logged-in user 
            // 获取当前登录用户的安全识别(security identity)
            $tokenStorage = $this->get('security.token_storage');
            $user = $tokenStorage->getToken()->getUser();
            $securityIdentity = UserSecurityIdentity::fromAccount($user);
 
            // grant owner access  / 授予所有者权限 
            $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
            $aclProvider->updateAcl($acl);
        }
    }
}

此码段中有几个重要的执行决策(implementation decisions)。现在我着重讲两处:

首先,你可能注意到 ->createAcl() 没有直接接收域对象,但它实现了 ObjectIdentityInterface 接口。这个间接的额外步骤可以使你在没有实际的域对象实例时也可以使用ACLs。当你要对海量对象检查权限却并不“水合(hydrating)”这些对象时,这极为有用。(译注:参考doctrine的 hydration,指结果集的返回格式)

另一个有趣的部分是调用 ->insertObjectAce()。在这个例子中,你正在对当前已登录为“所有者(owner)”的用户授予评论访问权。MaskBuilder::MASK_OWNER 是一个预定义的整型“位掩码”(integer bitmask);不必担心,掩码生成器(mask builder)会抽象大部分技术细节,通过这个技巧,你可以在一个数据行(one row)中存储很多不同的权限,由此获得巨大的性能提升。

检查ACEs时的顺序极其重要。作为一个通用原则,你从一开始就要放置更多的entries(译注:指ACEs)。

检查访问权限 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/AppBundle/Controller/BlogController.php
 
// ...
 
class BlogController
{
    // ...
 
    public function editCommentAction(Comment $comment)
    {
        $authorizationChecker = $this->get('security.authorization_checker');
 
        // check for edit access / 检查编辑权限
        if (false === $authorizationChecker->isGranted('EDIT', $comment)) {
            throw new AccessDeniedException();
        }
 
        // ... retrieve actual comment object, and do your editing here
        // ... 取出真正的评论对象,并在此处进行编辑
    }
}

本例中,你检查用户是否有 EDIT 权限。在内部,symfony把权限映射到各自的整型位掩码,然后检查用户是否拥任何一个权限。

你可以建立最大32个基本权限(根据操作系统之不同,PHP可能会有30-32个)。此外,你还可以定义权限叠加。

权限叠加 

在上面第一个例子中,你只授予用户 OWNER 基本权限(base permission)。尽管这可以确保让用户能够对域对象进行诸如 查看,编辑等 操作,但在某些情况下,你可能希望显式地指定这些权限。

通过组合若干基本权限,MaskBuilder 可用于轻松创建位掩码:

1
2
3
4
5
6
7
8
$builder = new MaskBuilder();
$builder
    ->add('view')
    ->add('edit')
    ->add('delete')
    ->add('undelete')
;
$mask = $builder->get(); // int(29)

此整型位掩码可用于对用户赋予你在上面添加了的基础权限:

1
2
$identity = new UserSecurityIdentity('johannes', 'AppBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);

从现在起,用户可以查看、编辑、删除和反删除对象了。

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

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