Shiro将Permission定义为定义显式行为或操作的语句。它是应用程序中原始功能的声明,仅此而已。权限是安全策略中最低级别的构造,它们只显式定义应用程序可以执行的操作。
他们根本没有描述“谁”能够执行行动((而不是描述who对what(which)进行how操作))。
一些权限示例:
- 打开一个文件
- 查看“/ user / list”网页
- 打印文件
- 删除’jsmith’用户
定义“谁”(用户)被允许做“什么”(权限)实际上是以某种方式向用户分配权限。这应该由应用程序的数据模型完成,并且在不同应用程序之间会有很大差异。
例如,权限可以在角色中分组,并且该角色可以与一个或多个用户对象相关联。或者某些应用程序可以拥有一组用户,并且可以为一个组分配一个角色,通过传递关联关系意味着该组中的所有用户都被隐式授予角色中的权限。
如何为用户授予权限有很多种方式 - 应用程序根据实际需求来确定如何对其进行建模。
通配符权限
上述权限示例中,“打开文件”,“查看’用户/列表’网页”等都是有效的权限声明。然而,在程序中解释那些由自然语言组成的字符串并确定是否允许用户执行该行为还是非常困难的。
因此,为了实现易于处理又同时拥有可读性的权限声明,Shiro提供了强大而直观的权限语法,我们将其称为WildcardPermission(通配符权限)。
简单用法
假设您希望添加对公司打印机的访问权限,以限制某些人可以打印到特定的打印机,而其他人可以查询当前队列。
一种非常简单的方法是授予用户“queryPrinter”权限。然后,您可以调用查看权限的方法来判断用户是否具有queryPrinter的权限:
subject.isPermitted("queryPrinter")
这相当于
subject.isPermitted( new WildcardPermission("queryPrinter") )
简单的字符串权限可能适用于简单的应用程序,但如果它要求您具有“printPrinter”,“queryPrinter”,“managePrinter”等权限。您还可以使用通配符授予用户“*”权限,这意味着它们拥有整个应用程序的所有权限。
但是使用这种方法,没有办法精准的描述用户只具有“所有打印机权限”。因此,通配符权限支持多级权限。
多级别
通配符权限支持多个级别或部分的概念。例如,您可以通过向用户授予权限来重构上一个简单示例
printer:query
此示例中的冒号是一个特殊字符,用于分隔权限字符串中的下一部分。
在此示例中,第一部分是操作的域(printer),第二部分是正在执行的action(query)。以上其他示例将更改为:
printer:print
printer:manage
多值
每个部分都可以包含多个值。因此,您不必向用户授予“printer:print”和“printer:query”权限,而只需授予他们一个权限:
printer:print,query
这使他们有能力print和query打印机。并且由于它们被授予这两个操作,您可以通过调用查看权限的方法来判断用户是否能够查询打印机:
subject.isPermitted("printer:query")
全部值
如果您想授予用户特定部分中的所有值,该怎么办?这样做比手动列出每个值更方便。同样,基于通配符,我们可以做到这一点。如果printer域有3个可能的行动(query,print,和manage),这一点:
printer:query,print,manage
简单地变成这样:
printer:*
然后,对于“printer:XXX”的任何权限检查将返回true。以这种方式使用通配符比显式列出所有操作属性更好扩展,因为如果后面向应用程序添加新操作,则不需要更新该部分中的通配符的权限。
最后,还可以在通配符形式的权限字符串的任何部分中使用通配符令牌。例如,如果您想要授予用户跨所有域(而不仅仅是打印机)的“查看”操作,您可以授予此权限:
*:view
然后对“foo:view”的任何权限检查返回 true
实例级访问控制
通配符权限的另一个常见用法是为实例级访问控制列表建模(精确到域中的单个实例)。在这种情况下,您使用三个部分 - 第一部分是域,第二部分是操作,第三部分是被操作的实例。
例如,你可以拥有
printer:query:lp7200
printer:print:epsoncolor
第一个定义的行为,是对ID为lp7200的printer有query权限。第二个权限定义ID为epsoncolor的printer有print权限。如果您将这些权限授予用户,则他们可以在特定实例上执行特定行为。例如:
if ( SecurityUtils.getSubject().isPermitted("printer:query:lp7200") {
// Return the current jobs on printer lp7200 }
}
这是表达权限的一种非常强大的方法。但同样,必须为所有打印机定义多个实例ID的方式并不能很好地扩展,特别是在将新打印机添加到系统时。您可以改为使用通配符:
printer:print:*
这样就方便了后续扩展,因为它也涵盖了所有新的打印机。您甚至可以允许访问所有打印机上的所有操作:
printer:*:*
或单个打印机上的所有操作:
printer:*:lp7200
甚至是具体行动:
printer:query,print:lp7200
'*‘通配符和’,'子部分分隔符可用于权限的任何部分。
缺少的部分
关于权限分配的最后一点需要注意:缺少部分意味着用户可以访问与该部分对应的所有值。换一种说法,
printer:print
相当于
printer:print:*
和
printer
相当于
printer:*:*
但是,您只能在字符串末尾留下部分内容,因此:
printer:lp7200
不等同于
printer:*:lp7200
检查权限
虽然权限分配使用通配符构造(“printer:print:*”=打印到任何打印机)以获得方便和可伸缩性,但运行时的权限检查应始终基于可能的最具体的权限字符串(越详细越好)。
例如,如果用户有UI并且他们想要将文档打印到lp7200打印机,则应该通过执行以下代码来检查是否允许用户这样做:
if ( SecurityUtils.getSubject().isPermitted("printer:print:lp7200") ) {
//print the document to the lp7200 printer }
}
该检查非常具体,并明确反映了用户在该时刻尝试做的事情。
但是,对于运行时检查,以下内容则不太理想:
if ( SecurityUtils.getSubject().isPermitted("printer:print") ) {
//print the document }
}
为什么?因为第二个例子说“您必须能够打印到任何打印机才能执行以下代码块”。但请记住,“printer:print”相当于“printer: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
时,当用户,角色和权限信息都缓存在内存中时会非常快。但随着分配给用户或其角色或组的权限数量的增加,执行检查的时间必然会增加。
如果Realm
的实现者有更有效的方法来检查权限并执行此隐含逻辑,特别是如果基于应用程序的数据模型,他们应该将其作为Realm isPermitted *
方法实现的一部分来实现。默认的Realm / WildcardPermission
支持占大多数用例的80-90%,但对于具有在运行时存储和/或检查的大量权限的应用程序,它可能不是最佳解决方案。