ACL 访问控制
Section 1 理解ACL是如何工作的
大部分重要的资源都需要访问控制。ACL(Access control lists)是一种细粒度,易维护和易管理的管理程序权限的方式。ACL主要处理两方面内容:一个是发起资源请求的对象,一个是希望获取的资源。用ACL的行话来说,请求使用资源的对象(通常是用户)称作请求访问对象,简称为ARO(access request object)。而希望获取的资源(通常为action和数据)被称作访问控制对象,简称为ACO(access control object)。我们将这些实体称之为'object'是因为有的时候发起请求的并不是用户本身,这种情况下你可以希望限制某些特定的controller的访问权限,而这些controller又不得不在应用程序的其他位置初始化逻辑。ACO可以是任何你希望进行访问控制的对象,从controller中的action到一个web服务接口,甚至是你祖母在线日记上的某一行。
一言以蔽之:ACL是用来定义一个ARO什么时候可以访问一个ACO。
为了帮助你更好的理解ACL,我们来看一个实际的例子。想象一下,我们的魔戒小队共同使用一台电脑。队长希望在最终决战之前保持任务的秘密和安全,具体的ARO如下:
Gandalf
Aragorn
Bilbo
Frodo
Gollum
Legolas
Gimli
Pippin
Merry
这些是系统中会发起访问请求的对象(ACO)。有一点需要说明:ACL 不是 一个用来验证用户合法性的系统。你应该已经有了保存用户信息和校验用户登录合法性的方法。当你知道了用户是谁后,ACL才开始发挥作用。好了,回到我们的双塔奇谋中。
我们的大法师甘道夫需要制作一张ACO的列表,确定有哪些需要控制的资源:
Weapons 武器
The One Ring 魔戒
Salted Pork 肉干
Diplomacy 外交协议
Ale 啤酒
传统情况下,我们会使用一种矩阵来管理这些,此矩阵展示了用户和与资源间相关对象的权限情况。如果此信息存储在一个表里,它可能会像下面这样,其中X表示拒绝访问,O代表允许访问:
Weapons The One Ring Salted Pork Diplomacy Ale
Gandalf X X O O O
Aragorn O X O O O
Bilbo X X X X O
Frodo X O X X O
Gollum X X O X X
Legolas O X O O O
Gimli O X O X X
Pippin X X X O O
Merry X X X X O
初看来,这个系统工作的非常好。这样的分配可以保证安全(只有Frodo可以碰魔戒),也可以防止一些意外(不让hobbits吃光所有的肉干)。看上去这个列表已经足够细致,并且简明易懂,但真的是这样吗?
对于一个小型系统,这样的矩阵列表可以完成任务。但是对于一个规模不断增长或者是拥有大量资源(ACO和ARO)的系统来说,这样一个表格将会变得非常笨重。比如我们试图控制上百个军营并且希望按组管理他们。矩阵的另一个缺点是你不能真正地按逻辑来将用户分组,并且按照逻辑分组对一组用户做出一连串的权限变更。比如,当战争一结束就能自动的允许Hobbit大快朵颐的享用肉干和啤酒,我们能很容易的改变一组用户的访问权限,为每一个用户创建独立的规则即乏味也不正确。
ACL是采用树状结构来实现的。通常ARO一棵树,ACO一棵树。使用树状结构来组织你的对象,权限仍然可以是细粒度的,而且同时可以对大局有很好的把握。最终成为白袍的甘道夫,当然明智的选择了使用ACL作为新的管理方式,结构如下:
Fellowship of the Ring:
Warriors
Aragorn
Legolas
Gimli
Wizards
Gandalf
Hobbits
Frodo
Bilbo
Merry
Pippin
Vistors
Gollum
按照这样的方式来组织我们的团队,我们可以定义这棵树的访问控制,并且应用这些权限到任意子节点上。默认的权限是禁止访问任何资源。
Fellowship of the Ring: [Deny: ALL]
Warriors [Allow: Weapons, Ale, Elven Rations, Salted Pork]
Aragorn
Legolas
Gimli
Wizards [Allow: Salted Pork, Diplomacy, Ale]
Gandalf
Hobbits [Allow: Ale]
Frodo
Bilbo
Merry
Pippin
Vistors [Allow: Salted Pork]
Gollum
如果我们希望知道Pippin是不是可以获得啤酒,我们首先要找出他在树种的路径,Fellowship->Hobbits->Pippin。我们会发现在3个节点拥有不同的属性,我们使用最明确的和Pippin,啤酒相关的权限。
Fellowship = DENY Ale, 禁止访问(因为默认情况时禁止访问任何资源)
Hobbits = ALLOW Ale, 允许访问
Pippin = ?; 没有明确定义,所以我们依然选择允许访问
最终结果:允许获得啤酒。
树结构也同样可以让我们非常方便的进行一些细粒度的调整,而且仍然具备了对ARO做出交替的变更:
Fellowship of the Ring: [Deny: ALL]
Warriors [Allow: Weapons, Ale, Elven Rations, Salted Pork]
Aragorn [Allow: Diplomacy]
Legolas
Gimli
Wizards [Allow: Salted Pork, Diplomacy, Ale]
Gandalf
Hobbits [Allow: Ale]
Frodo [Allow: Ring]
Bilbo
Merry [Deny: Ale]
Pippin [Allow: Diplomacy]
Vistors [Allow: Salted Pork]
Gollum
你可以看到,Aragorn比其他的战士多了使用外交协议的权限,这就是特别的微调。再说一下,默认的权限是禁止,并且只会在向下遍历树的时候被变更为允许。看一下Merry是不是可以获得啤酒,我们先查找她的路径:Fellowship->Hobbits->Merry 并查看对应的啤酒相关的权限:
Fellowship = DENY (默认禁止所有)
Hobbits = ALLOW: ale, 允许获得啤酒
Merry = DENY ale, 禁止获得啤酒
最终结果,禁止获得啤酒。
Section 2 定义权限:基于ini文件的ACL
Cake中第一种ACL实现是基于ini文件的。尽管ini文件非常有用并且稳定,我们仍然建议你使用基于数据库的ACL方案,因为这样就可以实时地创建新的ACO和ARO。即只在非常简单的小项目或者因为某些原因不能使用数据库的情况下使用这种ACL方案。
ARO/ACO 权限设定保存在/app/config/acl.ini.php文件中。在文件的最前面你可以找到使用方法:
; acl.ini.php - Cake ACL Configuration
; ---------------------------------------------------------------------
; Use this file to specify user permissions.
; aco = access control object (something in your application)
; aro = access request object (something requesting access)
;
; User records are added as follows:
;
; [uid]
; groups = group1, group2, group3
; allow = aco1, aco2, aco3
; deny = aco4, aco5, aco6
;
; Group records are added in a similar manner:
;
; [gid]
; allow = aco1, aco2, aco3
; deny = aco4, aco5, aco6
;
; The allow, deny, and groups sections are all optional.
; NOTE: groups names *cannot* ever be the same as usernames!
使用ini文件,你可以指定用户(ARO),用户所属的组和他们各自的权限。同样也可以设定组的权限。如何使用Cake ACL component来检查用户的权限,请参看11.4。
Section 3 定义权限:基于数据库的ACL
开始
默认的ACL方案是保存在数据库中的。dbACL由一组核心model和一个命令行脚本组成。Model用来和数据库交互以保存和获取ACL树。而命令行脚本可以帮助你创建数据库。
首先需要正确的配置你的数据库连接,具体请参考先前的章节。
然后使用Cake提供的命令行脚本来创建储存ACL的数据表,/cake/scripts/acl.php可以帮你完成这一系列操作。在命令行里/cake/scripts/目录下执行该命令:
$ php acl.php initdb
[TODO check in win env]
Initializing Database...
Creating access control objects table (acos)...
Creating access request objects table (acos)...
Creating relationships table (aros_acos)...
Done.
好了,你可以检查数据库看有没有新的表。如果你对Cake如何存储树状信息感到好奇,你可以研读一下修改过的数据库中树的遍历。基本上来说,它存储了节点以及它们在树中的位置。ACO和ARO分别在各自的树中保存这些节点,并且和model相对应。
现在你可以创建你的ARO ACO树了。
创建ARO和ACO
有两种方式来引用ARO或者ACO。一种是给它们一个ID,通常是他们对应的表的主键ID。另一种是给它们一个字符串别名。这两种方式并不是互斥的。
在Aro model中定义了创建新的ARO对象的方法:create()。该方法有3个参数:$link_id $parent_id $alias。此方法创建一个由$parent_id指定的父对象之下的ACL对象,如果传入的$parent_id为null,则作为一个根对象。$link_id允许你将当前用户对象链接到Cake的ACL结构。$alias参数允许你放入一个非整型ID的对象。
在你能够创建ACO和ARO之前,我们需要加载这些类。实现加载的最简单方法是使用$components数组,并将Cake ACL组件包含在Controller里:
var $components = array('Acl');
准备完毕,让我们看一个创建对象的例子,下列代码可以放置在一个action中:
$aro = new Aro();
// First, set up a few AROs.
// These objects will have no parent initially.
$aro->create( 1, null, 'Bob Marley' );
$aro->create( 2, null, 'Jimi Hendrix');
$aro->create( 3, null, 'George Washington');
$aro->create( 4, null, 'Abraham Lincoln');
// Now, we can make groups to organize these users:
// Notice that the IDs for these objects are 0, because
// they will never tie to users in our system
$aro->create(0, null, 'Presidents');
$aro->create(0, null, 'Artists');
//Now, hook AROs to their respective groups:
$aro->setParent('Presidents', 'George Washington');
$aro->setParent('Presidents', 'Abraham Lincoln');
$aro->setParent('Artists', 'Jimi Hendrix');
$aro->setParent('Artists', 'Bob Marley');
//In short, here is how to create an ARO:
$aro = new Aro();
$aro->create($user_id, $parent_id, $alias);
你也可以使用命令行方式创建ARO,$acl.php create aro
创建ACO的方式和ARO类似:
$aco = new Aco();
//Create some access control objects:
$aco->create(1, null, 'Electric Guitar');
$aco->create(2, null, 'United States Army');
$aco->create(3, null, 'Fans');
// I suppose we could create groups for these
// objects using setParent(), but we'll skip that
// for this particular example
//So, to create an ACO:
$aco = new Aco();
$aco->create($id, $parent, $alias);
对应的脚本命令为: $acl.php create aco
分配权限
创建好了ARO ACO,我们最后来为两者分配一下权限。这需要使用ACL component。让我们继续先前的例子:
// First, in a controller, we'll need access
// to Cake's ACL component:
class SomethingsController extends AppController
{
// You might want to place this in the AppController
// instead, but here works great too.
var $components = array('Acl');
// Remember: ACL will always deny something
// it doesn't have information on. If any
// checks were made on anything, it would
// be denied. Let's allow an ARO access to an ACO.
function someAction()
{
//ALLOW
// Here is how you grant an ARO full access to an ACO
$this->Acl->allow('Jimi Hendrix', 'Electric Guitar');
$this->Acl->allow('Bob Marley', 'Electric Guitar');
// We can also assign permissions to groups, remember?
$this->Acl->Allow('Presidents', 'United States Army');
// The allow() method has a third parameter, $action.
// You can specify partial access using this parameter.
// $action can be set to create, read, update or delete.
// If no action is specified, full access is assumed.
// Look, don't touch, gentlemen:
$this->Acl->allow('George Washington', 'Electric Guitar', 'read');
$this->Acl->allow('Abraham Lincoln', 'Electric Guitar', 'read');
//DENY
//Denies work in the same manner:
//When his term is up...
$this->Acl->deny('Abraham Lincoln', 'United States Army');
}
}
这个特别的controller并不是特别有用,主要是用来展示这个流程是怎么样的。在用户管理controller中使用ACL component会使最佳的场景。当创建了一个用户之后,他的ARO同时被创建并且放置在树中合适的位置,并且根据他的标识分配特定的ACO或者是ACO组。
可以使用与Cake一起打包的命令行脚本分配权限。此语法和model中的函数有些类似,而且可以通过执行$php acl.php帮助来查看它。
Section 4 校验权限:ACL Component
校验权限是ACL中最简单的部分了:只需要使用ACL Component中的check()函数就可以了。一个很好的实践就是在AppController中放置一个action来执行ACL权限校验。这样的好处是,可以在任何action中调用该方法来校验权限,我们看一下示例:
class AppController extends Controller
{
// Get our component
var $components = array('Acl');
function checkAccess($aco)
{
// Check access using the component:
$access = $this->Acl->check($this->Session->read('user_alias'), $aco, $action = "*");
//access denied
if ($access === false)
{
echo "access denied";
exit;
}
//access allowed
else
{
echo "access allowed";
exit;
}
}
}
基本上,通过在AppController基类里包含ACL component,从而使应用程序中的任何Controller中都可使用该方法。
$this->Acl->Check($aro, $aco, $action = '*');