带有访问权限的 Drupal 7 节点访问控制
有几种方法可以创建复杂的节点访问系统。分类法访问控制和节点访问等模块将允许您以不同方式限制节点访问,并且非常适合设置分类法或基于角色的访问控制。在一些边缘情况下,您需要根据一些任意条件(例如用户的年龄或字段的内容)来限制对节点的访问。这就是Drupal访问控制机制的构建发挥作用的地方。他们确实需要一些努力来了解他们的工作方式,但我希望在这篇文章中有所启发。
有两个钩子协同工作以促进这些节点级别的访问权限。这些钩子是hook_node_grants()和hook_node_access_records()。它们共同创建了一种锁和钥匙系统,拥有正确钥匙的用户可以使用正确的锁查看内容。这些钩子内置在Drupal核心中,无需安装任何额外的模块即可使用(除了用于实现钩子的模块)。
这些钩子的好处是它们可以被许多其他模块理解,因此应用于节点的任何权限都在整个站点中实现。这意味着您无需执行任何操作即可将权限包含在视图等模块中,因为此功能已内置。
锁创建
所有这一切的第一步是设置你的锁。在hook_node_access_records()保存节点时看到那种访问权限需要什么样的钩子使用Drupal的。更新节点权限时也会触发它。这个函数需要做的就是返回一个数组,告诉Drupal锁应该是什么样子。该数组需要包含以下项目:
nid:节点的ID。
领域:授权的领域,通常是模块名称。
gid:授权ID,这实际上是一种将用户与访问权限匹配的方式(稍后我们将介绍)。这必须是一个数值,但它可以是您想要的任何值。
grant_view:一个数值,用于确定用户查看节点的权限,1表示该用户是该组的一部分,应该被授予访问权限。这应该设置为节点的状态($node->status),因为存在可以查看未发布节点的危险。
grant_update:一个数值,用于确定用户更新节点的权限,1表示授予用户访问权限。
grant_delete:一个数值,用于确定用户删除节点的权限,1表示授予用户访问权限。
priority:授予的优先级。当多个模块相互争斗以授予或拒绝对节点的访问时,这就会发挥作用。在这种情况下,优先级越高越好。
Drupal的官方指南声明将优先级保留为0,但我认为将其设置为1可能是更好的方法。这可确保您自己的权限胜过任何正常的Drupal限制。但请注意,您自己的权限绝对胜过一切,因此您需要确保考虑已发布状态之类的内容,如果您仍然需要这些内容。同样重要的是要记住,如果调用钩子并返回一个空白数组,那么该内容项将获得默认访问限制。
调用此钩子后,Drupal会将数据写入名为node_access的表中。该表将包含钩子返回的数组项的内容。
最好不要仅仅展示这个钩子本身,最好是用一些上下文来显示被调用的钩子。假设我们有一个内容类型,其中包含一个名为“field_age_restriction”的字段,它是一个简单的布尔复选框。我们可以使用此复选框作为基于用户年龄限制对节点的访问的一种方式,因此我们使用hook_node_access_records()钩子生成用户在查看内容之前需要满足的访问权限。我们在这个钩子中所做的是确保我们有正确的内容类型,从节点中提取字段值,然后根据该字段的值设置访问数组。
/** * Implements hook_node_access_records(). */ function mymodule_node_access_records($node) { $grants = array(); //确保我们有正确的内容类型。 if ($node->type == 'article') { //提取“field_age_restriction”字段的值。 $content_age_restriction = field_get_items('node', $node, 'field_age_restriction', 'und'); if ($content_age_restriction !== FALSE) { $content_age_restriction = array_pop($content_age_restriction); if ($content_age_restriction['value'] == 1) { //如果我们有一个年龄限制节点,那么设置授权。 $grants[] = array( 'nid' => $node->nid, 'realm' => 'mymodule_age_restriction', 'gid' => 1, 'grant_view' => $node->status, 'grant_update' => 0, 'grant_delete' => 0, 'priority' => 1 ); } } } return $grants; }
假设一个节点已经发布并保存,并且启用了年龄限制,我们将拥有一个看起来像这样的锁。
node = 123 realm = mymodule_age_restriction gid = 1 view = true update = false delete = false
密钥创建
现在我们已经设置了锁,我们需要给每个用户一个锁的钥匙。我们使用名为的钩子来完成此操作,该钩子带有hook_node_grants()两个参数。
account:当前用户帐户对象。
op:对内容项执行的当前操作。这将是“查看”、“更新”或“删除”。
钩子应该返回用户有权访问的授权列表,这应该等同于在hook_node_access_records()钩子中设置的领域和gid。细心的读者可能会立即看到这个钩子中没有任何引用原始节点的东西(这个钩子没有$node参数)。这是有意为之,并且可能是此处涉及的过程中最难理解的事情。锁和钥匙的类比在这方面很合适,因为锁或钥匙中没有任何东西可以了解对方,只是钥匙具有适合锁的结构。给用户的键是从这个钩子返回的数组,它应该符合锁的结构,如定义hook_node_access_records().返回值需要有一个匹配领域的键和一个用户所属的gid数组。
在前面的示例的基础上,我们可以构建一个密钥,允许用户查看有年龄限制的内容。我们将为用户提供一个布尔值字段,表示他们已年满18岁。我们可以根据用户的出生日期实现一个日期字段来执行此操作,但出于演示目的,这个布尔值更易于理解。下面是hook_node_grants()钩子的一个实现。
/** * Implements hook_node_grants(). */ function mymodule_node_grants($account, $op) { $grants = array(); //设置默认授予条件。 $grants['mymodule_age_restriction'] = array(0); //处理“查看”操作。 if ($op == 'view') { //确保用户已登录。 if (user_is_logged_in() !== FALSE) { //提取“field_user_is_18”字段的值。 $current_user = user_load($account->uid); $user_is_18 = field_get_items('user', $current_user, 'field_user_is_18', 'und'); if ($user_is_18 !== FALSE) { $user_is_18 = array_pop($user_is_18); if ($user_is_18['value'] == 1) { //用户具有正确的值,允许他们使用'1'的gid访问。 $grants['mymodule_age_restriction'] = array(1); } } } } return $grants; }
我们在这里返回的是一个包含允许用户参与的gid的领域数组。所以在上面的例子中,领域是'mymodule_age_restriction'并且1的gid与我们在hook_node_access_records()钩子中设置的gid匹配。因此,如果我们假设用户已经检查了年龄限制的这个布尔字段,我们将为该用户提供一个看起来像这样的键。
realm = mymodule_age_restriction gid = 1 view = true
当应用于锁定时,可以看出这允许用户根据匹配的领域、gid和操作字段(可以是查看、更新或删除)查看内容。这也可以防止对编辑和删除节点的任何特殊访问。如果遗漏了任何权限,则该权限默认恢复为正常的Drupal权限,在编辑和删除的情况下为false。如果任何模块允许访问节点,则授予访问权限,无法从不同的钩子“取消设置”权限。
这个钩子只对需要权限的用户调用,并且Drupal超级用户(即用户编号1)将始终绕过这些访问检查。除此之外,如果用户已被授予“绕过节点访问”权限,那么他们也将绕过这些访问检查。
实现更复杂的条件
因为hook_node_grants()钩子的返回值是一个gid数组,我们可以返回多个可能匹配hook_node_access_records()钩子定义的不同gid的值。更进一步,我们可以展示如何通过本质上重新创建分类访问控制的功能并将用户与使用分类术语ID作为gid的节点匹配来利用它。
这是hook_node_access_records()钩子的一个实现,它创建一个访问锁,其中包含字段field_tags中包含的所有分类术语。
/** * Implements hook_node_access_records(). */ function mymodule_node_access_records($node) { $grants = array(); //确保我们有正确的内容类型。 if ($node->type == 'article') { //提取“field_tags”字段的值。 $content_tags_restriction = field_get_items('node', $node, 'field_tags', 'und'); if ($content_tags_restriction !== FALSE) { //因为我们可能有具有重复项的节点,所以我们需要创建一个唯一的数组或项。 $tags = array(); foreach ($content_tags_restriction as $tag) { $tags[] = $tag['tid']; } $tags = array_unique($tags); //将唯一条款应用于访问记录。 foreach ($tags as $tag){ $grants[] = array( 'nid' => $node->nid, 'realm' => 'mymodule_tags_restriction', 'gid' => $tag, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0, 'priority' => 1 ); } } } return $grants; }
假设在“field_tags”中为节点提供了三个分类术语,则会生成如下表所示的锁。
然后我们调整hook_node_grants()钩子以相同的方式使用分类法数组。我们使用的字段与此处节点上使用的字段相同。
/** * Implements hook_node_grants(). */ function mymodule_node_grants($account, $op) { $grants = array(); //设置默认授予条件。 $grants['mymodule_tags_restriction'] = array(0); //处理“查看”操作。 if ($op == 'view') { //确保用户已登录。 if (user_is_logged_in() !== FALSE) { //提取“field_tags”字段的值。 $current_user = user_load($account->uid); $user_tags = field_get_items('user', $current_user, 'field_tags', 'und'); if ($user_tags !== FALSE) { $grants['mymodule_tags_restriction'] = array(); foreach ($user_tags as $tag) { //将用户拥有的标签添加到标签列表中。 $grants['mymodule_tags_restriction'][] = $tag['tid']; } } } } return $grants; }
假设在field_tag字段中为用户提供了两个分类术语,我们会生成一个如下所示的键。
用户的术语之一与节点上的权限相匹配(对于分类术语“7”),因此授予用户查看此节点的权限。这现在可以作为用户的简单分类访问控制机制,并允许站点管理员根据分类条款轻松授予或拒绝对查看节点的访问权限。
调试
所有这些都很好,但是你到底要如何调试呢?当用户获得的访问权出现问题时,您该何去何从?事实证明,Devel模块以节点访问块的形式对此有所了解。打开“开发节点访问”模块将添加两个名为“开发节点访问”和“用户开发节点访问”的块。“开发节点访问”块将显示从节点本身生成的所有访问权限,这实质上显示了您创建的锁。'DevelNodeAccessbyUser'块将显示10个最近活跃的用户以及他们的权限与当前节点的关系。Devel没有为您提供有关如何实施权限的任何上下文hook_node_grants(),但如果用户无权访问该节点,您将看到文本“NO:noreason”,如果他们访问了,您将看到“YES:{node_access}”。
要记住的一件非常重要的事情是,只有在保存或更新节点时才会记录访问权限。这意味着当你改变你的hook_node_access_records()钩子时,没有节点会理解这个新结构,直到你保存它们。这可能意味着允许用户访问他们原本不会被允许看到的内容。幸运的是,Drupal中内置了一个函数,它将重新处理所有访问挂钩并基本上重新创建node_access表。重建权限管理功能(位于/admin/reports/status/rebuild)将在批处理中执行此操作,这可能是一个漫长的过程,具体取决于系统中有多少节点。
我应该指出有一个名为hook_node_access().这允许通过比较当前用户和当前节点来控制内容。不幸的是,此功能在Drupal的许多方面(例如主站点RSS提要和主页)都被遗漏了,因此您很快发现您必须设计代码来填补这些空白。例如,如果您要实现hook_node_access()调用,则需要进入所有视图和块,并确保它们理解实现的钩子中概述的相同权限。这可能很快导致受限制的内容可用,而实际上并不打算这样做。这个问题是看不到的hook_node_access_records()钩子,因为它可以通过使用数据库标记来引用。如果看到“node_access”标签,则Drupal将知道该查询包含应该受到限制的节点,并会强制用户在查看内容之前通过权限测试。默认情况下,视图将使用此数据库标记,因此您只需担心设置锁和钥匙,而无需担心Drupal站点的表示层中的间隙。