Apache Shiro中权限应用指南:深入理解权限
作者 崔传新
0.导引
Shiro定义权限与明确地界定或声明一个行为或操作一样。它是应用程序中原始功能的声明,仅此而已。权限是安全策略中的最低级别构造,它们仅仅明确定义应用程序可以执行的操作。
他们根本不描述或完全不在意"谁"能够执行这些行动。
这里列举一些权限示例,如下所示:
打开文件;
查看‘/user/list’页面;
打印文档;
删除‘jsmith’用户等。
定义"谁"(用户)被允许执行"什么"(权限)是以某种方式向用户分配权限的一种活动。 这总是由应用程序的数据模型来完成,并且可能因应用程序而有很大差异。
例如,权限可以分组到一个角色中,并且该角色可以与一个或多个用户对象相关联。或者某些应用程序可以拥有一组用户,并且可以为一个组分配一个角色,通过传递性关联,这将意味着该组中的所有用户都被隐式授予角色中的权限。
对于如何授予用户权限,有很多种变化形式 ——具体是由应用程序确定如何根据应用程序需求对其进行建模来决定的。
1.通配符权限
上面的权限示例,"打开文件"、"查看'user/list'网页"等都是有效的权限声明。 然而,解释这些自然语言字符串并确定用户是否被允许执行该行为,计算上将是非常困难的。
因此,为了获得易于处理但仍然可读的权限声明的能力,Shiro提供了强大而直观的权限语法,我们称之为通配符权限(WildcardPermission)。
1.1简单用法
假设您希望保护对公司打印机的访问权限,以便某些人可以在特定打印机打印,而同时其他人可以查询当前正在排队的任务。
一个非常简单的方法是授予用户一个"queryPrinter"权限。 然后,您可以通过调用来查看用户是否具有queryPrinter权限:
subject.isPermitted("queryPrinter")
这(几乎)相当于如下语句:
subject.isPermitted( new WildcardPermission("queryPrinter") )
稍后将有更多关于这方面的内容(但后一句更内涵更多)。
简单的权限字符串可能适用于简单的应用程序,但它需要您具有诸如"printPrinter","queryPrinter","managePrinter"等权限。您还可以使用通配符授予用户"*"权限(授予所有‘名称化’权限),这意味着他们拥有整个应用程序的所有权限。
但是使用这种方法没有办法仅仅说用户具有"所有打印机权限"。出于这个原因,通配符权限支持多级权限(多部件构成)。
1.2多部件权限
通配符权限支持多个级别或部件的概念。 例如,您可以通过授予用户权限来重构前面的简单示例,如:
printer:query
本例中的冒号(英文字符)是一个特殊字符,用于分隔权限字符串中的下一部分。
在本例中,第一部分是正在操作的域(打印机),第二部分是正在执行的操作(查询)。上面的其他例子可改为:
printer:print
printer:manage
对于可以使用的部件数量没有限制,因此,在应用中采用的方式完全取决于您的想象力。
1.3多部件值
每个部分可以包含多个值。 因此,您不需要授予用户"printer:print"和"printer:query"权限,只需授予他们一个:
printer:print,query
这使他们能够打印和查询打印机。 并且由于它们被授予了这两项操作,因此您可以检查用户是否有能力通过调用来查询打印机,方式如下:
subject.isPermitted("printer:query")
这应该返回true值。
1.4部件所有值
如果你想授予用户某个特定部分的所有值,该怎么办? 这样做比手动列出每个值更方便。 再次,基于通配符,我们可以做到这一点。 如果printer域有3种可能的操作(query,print和manage),则这个实现方式如下:
printer:query,print,manage
简化为:
printer:*
然后,对"printer:XXX"的任何权限检查都将返回true。 以这种方式使用通配符比明确列出动作要好,因为如果稍后将新动作添加到应用程序中,则不需要更新该部分中使用通配符的权限。
最后,还可以在通配符权限字符串的任何部分使用通配符标记。 例如,如果您想授予用户所有域(不仅是打印机)的"查看"操作,您可以这样授予此权限:
*:view
然后,对"foo:view"的任何权限检查都会返回true。
2.实例级访问控制
通配符权限的另一个常见用法是为实例级访问控制列表(instance-level Access Control Lists)建模。 在这种情况下,您使用三部分:第一部分是域,第二部分是操作,第三部分是正在执行的实例。
例如,你可以这样:
printer:query:lp7200
printer:print:epsoncolor
第一个定义了查看ID为lp7200打印机的行为。 第二个权限定义了ID为epsoncolor的打印机的打印行为。 如果您将这些权限授予用户,那么他们可以在特定实例上执行特定行为。然后你可以在代码里做一个检查,如下:
if(SecurityUtils.getSubject().isPermitted("printer:query:lp7200")){
// Return the current jobs on printerlp7200
//(返回lp7200打印机当前作业及必要处理)
}
这是表达权限的一种非常强大的方式。 但是,必须为所有打印机定义多个实例ID并不能很好地进行扩展,特别是在将新打印机添加到系统中时。这一问题,你可以改为使用通配符来更好的实现:
printer:print:*
这确实可以扩展,以便它也覆盖了所有新的打印机。 您甚至可以允许访问所有打印机上的所有操作,操作如下:
printer:*:*
或者单个打印机上的所有操作:
printer:*:lp7200
甚至是单个打印机特定行为:
printer:query,print:lp7200
'*'通配符和','子部分隔符可用于权限的任何部分。
2.1缺失部件
最后要注意的是权限分配:缺少的部分(部件)意味着用户可以访问与该部分相对应的所有值。换一种说法:
printer:print与 printer:print:*是相等的。
printer与printer:*:*是相等的。
但是,您只能从字符串的末尾去掉部分(部件),所以下面是不相等:
printer:lp7200与printer:*:lp7200是不相等的。
3.检查权限
尽管为了方便和可扩展性,权限分配使用通配符构造相当多("printer:print:*"=到任何打印机的打印),但运行时的权限检查应始终基于最具体的权限字符串。
例如,如果用户有一个UI,并且他们想要将文档在lp7200打印机上打印,则应该检查用户是否被允许这样做,代码检查此操作如下:
if(SecurityUtils.getSubject().isPermitted("printer:print:lp7200")) {
//print the document to the lp7200 printer
//在lp7200打印机上打印文档
}
该检查非常具体,明确反映了用户当时正试图做的事情。
然而,对于运行时检查来说,下面的内容不太理想,也是不对的:
if(SecurityUtils.getSubject().isPermitted("printer:print") ) {
//print the document 打印文档
}
为什么? 因为第二个例子说"您必须能够在任何打印机上打印"。也请记住,"printer:print"相当于"printer:print:*"!
因此,这是一个不正确的检查。 如果当前用户无法打印到任何打印机,但他们确实有能力在lp7200和epsoncolor打印机上打印。那么上面的第二个例子永远不会允许他们打印到lp7200打印机,即使他们已被授予该能力!
因此,经验法则是在执行权限检查时使用最具体的权限字符串。 当然,如果你真的只想执行代码块,如果允许用户打印到任何打印机(怀疑,但可能),上面的第二个块可能是应用程序中其他地方的有效检查。您的应用程序确定哪些检查是有意义的,但总的来说,越具体越好。
4.隐含而不相等
为什么运行时权限检查应尽可能具体,但权限分配可更宽泛一些? 这是因为权限检查是通过蕴含逻辑进行评估的,而不是相等性检查。
也就是说,如果用户被分配了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
5.性能考虑
权限检查比简单的等同比较更复杂,因此运行时隐含逻辑必须针对每个分配的权限执行。当使用上面显示的权限字符串时,你隐式地使用Shiro的默认WildcardPermission来执行了必要的隐含逻辑。
Shiro对Realm实现的默认行为是,对于每个权限检查(例如,调用subject.isPermitted),需要分别隐式检查分配给该用户的所有权限(在其组、角色或直接分配给它们)中的所有权限。 Shiro通过在第一次成功检查后立即返回来"短路"这一过程(即检查到了就返回,不在检查其它),以提高性能,但这不是银弹。
当使用正确的CacheManager(Shiro支持Realm实现)时,用户、角色和权限被缓存在内存中时,这通常是非常快的。只要知道,使用此默认行为,随着分配给用户或其角色或组的权限数量增加,执行检查的时间必然会增加。
如果Realm实现者有更有效的方法来检查权限并执行这个隐含逻辑,特别是基于应用程序的数据模型,他们会将它作为其领域isPermitted方法的一部分来实现。默认情况下,Realm/WildcardPermission支持覆盖了大多数用例的80-90%,但它可能不是用于在运行时存储和/或检查拥有大量权限的应用程序的最佳解决方案。