7. Permissions 权限

Shiro定义了一个许可声明,定义了一个明确的行为或行动。 这是一个原始功能的声明在一个应用程序而已。 权限是最低级别构造安全策略,他们只明确定义应用程序可以做“什么”。他们不描述"谁"能够执行的操作。一些权限的例子:

  • 打开一个文件
  • 浏览'/user/list' 网页
  • 打印文件
  • 删除用户‘jsmith’

规定“谁”(用户)允许做“什么”(权限)在某种程度上是分配用权限的一种习惯做法。这始终是通过应用程序数据模型来完成的,并且在不同应用程序之间差异很大。

例如,权限可以组合到一个角色中,且该角色能够关联一个或多个用户对象。或者某些应用程序能够拥有一组用户,且这个组可以被分配一个角色,通过传递的关联,意味着所有在该组的用户隐式地获得了该角色的权限。

如何授予用户权限可以有很多变化——应用程序基于应用需求来决定如何使其模型化。

通配符的权限

上述权限的例子,“打开文件”、“浏览'/user/list' 网页”,等都是有效的权限。 然而,计算来解释这些自然语言字符串和确定用户是否允许执行这一行为这将是非常困难的。为了使更容易处理但仍可读权限语句,Shiro 提供强大的和直观的语法我们称之为 WildcardPermission 。

Simple Usage 简单示例

你想保护访问贵公司的打印机,这样有些人可以打印到特定的打印机,而其他人可以查询什么工作目前在队列中。一个非常简单的方法是使用授予用户“queryPrinter”权限。 然后你可以通过调用检查用户是否有 queryPrinter 权限:

subject.isPermitted("queryPrinter")

这是(大部分)相当于

subject.isPermitted( new WildcardPermission("queryPrinter") )

但远不只这些。简单的权限字符串可能在简单的应用程序中工作的很好,但它需要你拥有像"printPrinter","queryPrinter","managePrinter"等权限。你还可以通过使用通配符授予用户"*"权限(赋予此权限构造它的名字),这意味着他们在整个应用程序中拥有了所有的权限。但使用这种方法没有办法说明用户具有“所有打印机权限”。 出于这个原因,,Wildcard Permissions(通配符权限)支持多层次的权限管理。

多个部分

通配符权限支持多层次或部件(parts)的概念。例如,你可以通过授予用户权限来调整之前那个简单的例子

printer:query

在这个例子中的冒号是一个特殊字符,它用来分隔权限字符串的下一部件。在该例中,第一部分是权限被操作的领域(printer),第二部分是被执行的操作(query)。上面其他的例子将被改为:

printer:print
printer:manage

对于能够使用的部件是没有数量限制的,因此它取决于你的想象,依据你可能在你的应用程序中使用的方法

多值

每个部件能够保护多个值。因此,除了授予用户 "printer:print" 和 "printer:query" 权限外,你可以简单地授予他们一个:

printer:print,query

它能够赋予用户 print 和query 打印机的能力。由于他们被授予了这两个操作,你可以通过调用下面的语句来判断用户是否有能力查询打印机:

subject.isPermitted("printer:query")

该语句将会返回true

所有值

如果你想在一个特定的部件给某一用户授予所有的值呢?这将是比手动列出每个值更为方便的事情。同样,基于通配符的话,我也可以做到这一点。若打印机域有3 个可能的操作(query,print 和manage),可以像下面这样:

printer:query,print,manage

简单点变成这样:

printer:*

然后,任何对 "printer:XXX" 的权限检查都将返回 true。以这种方式使用的通配符比明确地列出操作具有更好的尺度,如果你不久为应用程序增加了一个新的操作,你不需要更新使用通配符那部分的权限。

最后,在一个通配符权限字符串中的任何部分使用通配符 token 也是可以的。例如,如果你想对某个用户在所有领域(不仅仅是打印机)授"view"权限,你可以这样做:

*:view

这样任何对"foo:view"的权限检查都将返回true。

实例级的访问控制

另一种常见的通配符权限用法是塑造实例级的访问控制列表。在这种情况下,你使用三个部件——第一个是域,第二个是操作,第三个是被付诸实施的实例。简单例子:

printer:query:lp7200
printer:print:epsoncolor

第一个定义了查询拥有ID lp7200 的打印机的行为。第二条权限定义了打印到拥有 ID epsoncolor 的打印机的行为。如果你授予这些权限给用户,那么他们能够在特定的实例上执行特定的行为。然后你可以在代码中做一个检查:

if ( SecurityUtils.getSubject().isPermitted("printer:query:lp7200") {
    // 返回ID lp7200 的打印机的当前任务
}

这是体现权限的一个极为有效的方法。但同样,为所有的打印机定义多个实例ID 能很好的扩展,尤其是当新的打印机添加到系统的时候。你可以使用通配符来代替:

printer:print:*

这个做到了扩展,因为它同时涵盖了任何新的打印机。你甚至可以运行访问所有打印机上的所有操作 printer:*:*。或在一台打印机上的所有操作:printer:*:lp7200 或甚至特定的操作:printer:query, print:lp7200*通配符,,子部件分离器可用于权限的任何部分。

缺省的部分

最后要注意的是权限分配:缺省的部分意味着用户可以访问所有与之匹配的值,换句话说 printer:print 等价于 printer:print:* 并且 printer 等价于 printer:*:*

然而,你只能从字符串的结尾处省略部件,因此这样的:printer:lp7200 并不等价于 printer:*:lp7200

检查权限

虽然权限分配使用通配符较为方便且具有扩展性("printer:print:*" = print to any printer),但在运行时的权限检查应该始终基于大多数具体的权限字符串。

例如,如果用户有一个用户界面,他们想打印一份文档到 lp7200 打印机,你应该通过执行这段代码来检查用户是否被允许这样做:

if ( SecurityUtils.getSubject().isPermitted("printer:print:lp7200") ) {
    //用 lp7200 printer 打印文档
}

这种检查非常具体和明确地反映了用户在那一时刻试图做的事情。 然而,下面这个运行是检查是不为理想的:

if ( SecurityUtils.getSubject().isPermitted("printer:print") ) {
    //打印文档
}

为什么?因为第二个例子表明“对于下面的代码块的执行,你必须能够打印到任何打印机”。但请记住 "pinter:print" 是等价 "priner:print:*" 的!

因此,这是一个不正确的检查。如果当前用户不具备打印到任何打印机的能力,仅仅只有打印到 lp7200 和 epsoncolor 的能力,该怎么办呢?那么上面的第二个例子也绝不允许他们打印到 lp7200 打印机,即使他们已被赋予了相应的能力!

因此,经验法则是在执行权限检查时,尽可能使用权限字符串。当然,上面的第二块可能是在应用程序中别处的一个有效检查,如果你真的想要执行该代码块,如果用户被允许打印到任何打印机(令人怀疑的,但有可能)。你的应用程序将决定检查哪些有意义,但一般情况下,越具体越好。

蕴涵,不相等

为什么运行时权限检查应该尽可能的具体,但权限分配可以较为普通?这是因为权限检查是通过蕴含的逻辑来判断的——而不是通过相等检查。

也就是说,如果一个用户被分配了 user:* 权限,这意味着该用户可以执行 user:view 操作。"user:* " 字符串明显不等于 "user:view",但前者包含了后者。"user:*" 描述了 "user:view" 所定义的功能的一个超集。

为了支持蕴含规则,所有的权限都被翻译到实现org.apache.shiro.authz.Permission 接口的的对象实例中。这是以便 蕴含逻辑能够在运行时执行,且蕴含逻辑通常比一个简单的字符串相等检查更为复杂。所有在本文档中描述的通配符行为实际上是由org.apache.shiro.authz.permission.WildcardPermission 类实现的。下面是更多的一些通过蕴含逻辑访问的通配符权限字符串:

user:* 同时蕴含着删除一个用户的能力: user:delete。 同样地,user:*:12345 同时蕴含着更新 ID 为12345 的用户帐户的能力:user:update:12345 而且 printer 蕴含着打印到任何打印机的能力 printer:print

性能考虑

权限检查比简单的相等比较要复杂得多,因此运行时的蕴含逻辑必须执行每个分配的权限。当使用像上面展示的权限字符串时,你正在隐式地使用 Shiro 默认的 WildcardPermission,它能够执行必要的蕴含逻辑。

Shiro 对 Realm 实现的默认行为是,对于每一个权限验证(例如,调用subject.isPermitted),所有分配给该用户的权限(在他们的组,角色中,或直接分配给他们)需要为蕴含逻辑进行单独的检查。Shiro 通过首次成功检查立即返回来“短路”该进程以提高性能,但它不是一颗银弹。

这通常是极快的,当用户,角色和权限缓存在内存中且使用了一个合适的CacheManager 时,在Shiro 不支持的 Realm 实现中。只要知道使用此默认行为,当权限分配给用户或他们的角色或组增加时,执行检查的时间一定会增加。

如果一个 Realm 的实现者有一个更为高效的方式来检查权限并执行蕴含逻辑,尤其它如果是基于应用程序数据模型的,他们应该实现它作为 Realm isPermitted* 方法实现的一部分。默认的 Realm/WildcardPermission 存在的支持覆盖了大多数用例的80~90%,但它可能不是在运行时拥有大量权限需要存储或检查的应用程序的最佳解决方案。