在我的上一篇文章中,我讨论了使用Zend_Form作为模型中的组合输入过滤器/值对象。在本文中,我将讨论使用访问控制列表(ACL)作为建模策略的一部分。
ACL用于指示谁有权对给定资源执行操作。在我要提出的范式中,你的资源就是你的模型,什么就是模型的各种方法。如果你稍微巧妙一点,你将拥有充当你的谁的“用户”对象。
就像表单一样,您希望将ACL尽可能靠近域逻辑;事实上,ACL是您域的一部分。
不过,首先让我们回顾一下Zend_Acl
。
Zend_Acl简述
Zend_Acl
分为三个职责范围:
- 资源是访问受控的对象
- 角色是可以请求访问一个或多个资源的对象
- ACL提供了一个树结构,可以向其中添加资源和角色,并在它们之间映射访问规则。
Zend_Acl
主要设计为以编程方式配置和操作。虽然您当然可以编写功能来从数据存储中提取信息——例如,LDAP目录或数据库——但在许多情况下,您不需要这样做。让我们看一下这个简单的ACL定义:
class Spindle_Model_Acl_Spindle extends Zend_Acl { public function __construct() { // Define roles: $this->addRole(new Spindle_Model_Acl_Role_Guest) ->addRole(new Spindle_Model_Acl_Role_User, 'guest') ->addRole(new Spindle_Model_Acl_Role_Developer, 'user') ->addRole(new Spindle_Model_Acl_Role_Manager, 'developer'); // Deny privileges by default; i.e., create a whitelist $this->deny(); // Define resources and add privileges $this->add(new Spindle_Model_Acl_Resource_Bug) ->allow('guest', 'bug', array('list', 'view')) ->allow('user', 'bug', array('add', 'comment', 'link', 'close')) ->allow('developer', 'bug', array('update', 'delete')); $this->add(new Spindle_Model_Acl_Resource_Comment) ->allow('guest', 'comment', array('view', 'list')) ->allow('user', 'comment', array('add')) ->allow('developer', 'comment', array('delete')); } }
在这个例子中,我们做了几件事:
- 定义我们的角色。您会注意到几个角色定义带有一个额外的参数。在每种情况下,此参数都指定新角色继承的角色。因此,当我们为一个角色应用特权时,从该角色继承的任何角色也将获得这些特权。
- 创建白名单。
deny()
方法,当在任何其他权限之前被调用时,告诉Zend_Acl
我们想要拒绝权限,除非我们特别允许它。 - 添加资源。
- 根据访问资源的角色指定每个资源的可用权限。这是通过
allow()
方法完成的。
Zend_Acl
中的
资源和角色只需要实现适当的接口。这些接口仅仅分别定义了一个方法,每个方法返回一个字符串标识符,该标识符在Zend_Acl
中的对象图中使用。例如:
// A simple role: class Spindle_Model_Acl_Role_Guest implements Zend_Acl_Role_Interface { public function getRoleId() { return 'guest'; } } // A simple resource: class Spindle_Model_Acl_Resource_Bug implements Zend_Acl_Resource_Interface { public function getResourceId() { return 'bug'; } }
您可能会注意到,这些实现起来很简单—关键是它们可以混合到您的模型类中以赋予它们语义意义。也就是说,有一个警告:在定义实际的ACL规则(映射角色和资源)时,指定的角色和资源必须已经存在在ACL树中。因此,我发现尽早定义我的角色,然后临时添加资源和权限很方便。
通过将基本ACL定义分组到一个对象中,我们现在有了一个可重复使用的ACL,我们可以传递它或在其他上下文中使用它,最终将我们带到我们的模型中。
在模型中使用Zend_Acl
角色
通常在ZendFramework中,您将使用Zend_Auth
对用户进行身份验证,这将在会话中保留他们的“身份”。这个“身份”可以是任何东西:字符串、数组、对象。后者提供了一些奇妙的潜力:如果对象实现了Zend_Acl_Role_Interface
,那么它可以用于ACL检查。
让我们定义一个实现角色接口的“用户”对象。在内部,我们会将用户定义的角色存储为对象的一部分,并让getRoleId()
方法返回该值。
class Spindle_Model_UserManager_User implements Zend_Acl_Role_Interface { /* ... */ public function getRoleId() { if (!isset($this->role)) { return 'guest'; } return $this->role; } /* ... */ }
您会注意到,这不仅提供了用户的当前角色,而且还提供了未设置时的应急措施(“访客”是我们的最低访问级别)。
我将在以后的文章中重新访问这个用户类。
资源
模型是一种资源。因此,它应该实现资源接口。此外,它可能应该知道允许哪些角色哪些权限。最后,它应该能够在执行操作之前验证访问权限。所以,我们需要一些代码。
首先,让我们的模型成为资源。
class Spindle_Model_BugTracker implements Zend_Acl_Resource_Interface { public function getResourceId() { return 'bug'; } /* ... */ }
现在,让我们允许注入一个ACL对象,或者如果没有找到则延迟加载它。在每种情况下,我们都应该为我们的资源设置访问列表。我们会将ACL对象限制为已知类型之一——这可确保出现特定角色。
class Spindle_Model_BugTracker implements Zend_Acl_Resource_Interface { /* ... */ protected $_acl; public function setAcl(Spindle_Model_Acl_Spindle $acl) { if (!$acl->has($this->getResourceId())) { $acl->add($this) ->allow('guest', $this, array('list', 'view')) ->allow('user', $this, array('save', 'comment', 'link', 'close')) ->allow('developer', $this, array('delete')); } $this->_acl = $acl; return $this; } public function getAcl() { if (null === $this->_acl) { $this->setAcl(new Spindle_Model_Acl_Spindle()); } return $this->_acl; } /* ... */ }
您会注意到我们将$this
作为参数传递。我们可以这样做,因为我们的模型是一种资源。另请注意,如果没有注入,我们将延迟加载ACL对象。
接下来,我们需要一种方法来确定当前角色。如前所述,在讨论角色时,您通常会使用Zend_Auth
来验证用户,这将保留当前身份。我们将允许注入当前身份,以及一种从Zend_Auth
延迟加载它的方法。
class Spindle_Model_BugTracker implements Zend_Acl_Resource_Interface { /* ... */ protected $_identity; public function setIdentity($identity) { if (is_array($identity)) { if (!isset($identity['role'])) { $identity['role'] = 'guest'; } $identity = new Zend_Acl_Role($identity['role']); } elseif (is_scalar($identity) && !is_bool($identity)) { $identity = new Zend_Acl_Role($identity); } elseif (null === $identity) { $identity = new Zend_Acl_Role('guest'); } elseif (!$identity implements Zend_Acl_Role_Interface) { throw new Spindle_Model_Exception('Invalid identity provided'); } $this->_identity = $identity; return $this; } public function getIdentity() { if (null === $this->_identity) { $auth = Zend_Auth::getInstance(); if (!$auth->hasIdentity()) { return 'guest'; } $this->setIdentity($auth->getIdentity()); } return $this->_identity; } /* ... */ }
您会注意到setIdentity()
有相当多的逻辑—因为身份可以是任意的,我们需要确保它可用于我们的目的。
现在我们有了自己的角色和资源,我们可以解决如何在我们的方法中添加检查以在执行代码之前验证用户权限的问题。
一个权宜之计的方法是使用__call()
拦截公共方法调用并将它们代理给受保护的成员。然而,这具有代码模糊和工具(IDE、ctags等)无法接收方法调用的负面影响。因此,让我们构建一个可用于检查ACL的辅助方法;然后每个方法将负责调用它并根据它的建议采取行动。
class Spindle_Model_BugTracker implements Zend_Acl_Resource_Interface { /* ... */ public function checkAcl($action) { return $this->getAcl()->isAllowed( $this->getIdentity(), $this, $action ); } }
现在,让我们将它连接到各种方法中。例如,请考虑我之前关于将表单与模型结合使用的条目中的save()
示例。我们可能会将请求的操作命名为“保存”,然后对其进行查询。那么我们需要做一个决定:如果用户没有权限,我们如何表示呢?常见的解决方案包括:
- 抛出异常
- 唯一返回值
- 唯一返回值+在对象中标记错误条件
我们将把权限不足视为此示例的例外情况:
class Spindle_Model_BugTracker implements Zend_Acl_Resource_Interface { /* ... */ public function save(array $data) { if (!$this->checkAcl('save')) { throw new Spindle_Model_Acl_Exception(\"Insufficient rights\"); } /* ... */ } /* ... */ }
现在实例化我们的模型时,我们需要传入当前标识,或者在实例化之后但在调用ACL控制的操作之前设置它:
// At instantiation: $bugModel = new Spindle_Model_BugTracker(array('identity' => $user)); // Following instantiation: $bugModel = new Spindle_Model_BugTracker(); $bugModel->setIdentity($user); $bugModel->save($data);
(当然,它也会自动从身份验证会话中提取它,但很高兴知道我们也可以注入它!)
再访ACL
既然资源和权限定义已经移到模型中,我们可以稍微简化实际的ACL对象,使其只定义角色并初始化白名单:
class Spindle_Model_Acl_Spindle extends Zend_Acl { public function __construct() { // Define roles: $this->addRole(new Spindle_Model_Acl_Role_Guest) ->addRole(new Spindle_Model_Acl_Role_User, 'guest') ->addRole(new Spindle_Model_Acl_Role_Developer, 'user') ->addRole(new Spindle_Model_Acl_Role_Manager, 'developer'); // Deny privileges by default; i.e., create a whitelist $this->deny(); } }
我们仍然在这里定义角色,因为我们的用户对象仅用于验证访问;我们仍然需要首先定义角色。
总结
Zend_Acl
非常简单和灵活。通过在您的模型中使用组合,您可以轻松地将ACL添加到您的域工作流中,有助于保持职责分离,同时不会失去一组好的ACL提供的功能。重要的一点是,ACL应该是您的模型逻辑的一部分,并且您可以使用对象组合来实现这一目标。
在下一期中,我将探讨“返回值也是模型的一部分”。