理解Apache Shiro中的权限
在Shiro中,权限代表了安全策略中最基本的元素,它们显式地表示在应用程序中可以做哪些事情,一个格式良好的权限语句,应该是描述了某个资源,以及当主体(Subject
)与这些资源交互时可能发生的操作,例如以下权限语句
- 打开一个文件
- 浏览 ”/user/list" web 页面
- 打印文档
- 删除 "jsmith" 用户
大多数资源都应该会支持CRUD
操作,任何对特定资源类型有意义的操作都是可以的,所以权限语句至少是基于资源和操作的,因此,在查看权限时,需要意识到权限没有表示谁可以执行所表示的行为,它们只是表示了在应用程序中可以做什么
权限仅仅代表行为,它们只是反映了对特定资源类型的操作,并不表示谁可以执行这些操作
允许谁(用户)做什么(权限)即为授权,这应该取决于你的应用程序的数据模型,不同的应用程序会有很大的不同
比如可以将多个权限集合起来可以组成一个角色,角色可以赋予给多个用户,这样被赋予角色的用户就拥有了该角色中的所有权限;也可以把多个用户组成一个用户组,给这个用户组赋予角色,该组中的所有用户都被隐式地授予了角色中的权限。对于如何将权限授予用户有许多不同的方式,应该根据需求确定如何对此进行建模
Wildcard Permissions
上面表述的权限的例子,如“打开文件”,“查看‘user/list'网页”,都是有效的权限字符串,但是,以自然语言的方式来表示的权限是难以理解,所以Shiro提供了强大且直观的权限语句的语法,即WildcardPermission
,来保证权限语句可读并且易于使用
简单使用
假设以下场景,在公司中有许多打印机(printer
),一些人可以在部分打印机上进行打印,其他人可以查看当前打印任务队列中还有哪些
一个简单的方式即赋予用户一个“queryPrinter
”权限,这样就可以调用下面的代码来检查用户是否有queryPrinter
权限:
subject.isPermitted("queryPrinter");
这和以下的代码是等效的
subject.isPermitted(new WildcardPermission("queryPrinter"));
当然,这个例子需要你的应用程序中有诸如“printPrinter
”,“queryPrinter
”,“managePrinter
”等权限,也可以通过通配符“*
”来表示拥有整个应用程序中的所有权限,但是使用这种方法无法让一个用户拥有所有的打印机权限。因为WildcardPermission
支持多级权限
多级权限字符串
WildcardPermission
支持多级权限的概念,上面的例子可以构造成如下的权限字符串
printer:query
冒号作为权限字符串各个部分的分隔符,上面的例子中,第一部分表示资源类型(printer)
,第二部分表示操作(query)
,其他的例子如下:
printer:print
printer:manager
一个权限字符串没有具体要求应该分为几部分,这取决于在应用程序中使用的方式
一个部分有多个值
权限字符串分隔的每一部分可以包含多个值,每个值之间用逗号分隔,比如需要给用户同时赋予“printer:print
”和“printer:query
”权限,可以简化为:
printer:print,query
如果用户已经有了上面的权限,则下面的代码将返回true
subject.isPermitted("printer:query")
一个部分的所有值
如果想把某一部分的所有值都赋予给用户,则可以采用通配符“*
”的形式,比如上面的打印机资源总共有三种操作(query,print和manage
),则可以将所有值都列出来然后把权限授予用户
printer:query,print,manage
也可以简写为:
printer:*
这样,任何形如"printer:XXX
"的权限检查都会返回true
。通配符可以出现在权限字符串的任一部分,用以表示该部分的所有值
实例级访问控制
Shiro支持实例级的访问控制,也是以冒号分隔,其中第一部分为资源类型,第二部分为操作,第三部分为实例,如下
printer:query:lp7200
printer:print:epsoncolor
第一个定义了ID为lp7200
的printer
类型的查询权限,第二个定义了ID为epsoncolor
的printer
类型的打印权限。这样就可以实现实例级别的授权,如下代码演示了这种授权的权限检查方式
if (SecurityUtils.getSubject().isPermitted("printer:query:lp7200")){
// Return the current jobs on printer lp7200
}
这种授权虽然强大,但是不具备扩展性,如果增加了新的打印机,则需要定义新的权限,这时可以结合通配符来解决,如下:
printer:print:*
printer:*:*
printer:*:lp7200
printer:query,print:lp7200
默认值
如果权限字符串的某一部分没有,则其默认为通配符“*
”,如
# 这两个权限字符串是等效的
printer:print
printer:print:*
# 这两个权限字符串是等效的
printer
printer:*:*
# 但是缺省值只能是权限字符串的末端开始,如下面两个权限就不是等效的
printer:lp7200
printer:*:lp7200
权限检查
为了遍历和扩展性,权限可以采用通配符的方式,但是在进行权限检查时,我们应该总是基于最小权限进行检查
举个例子,如果一个用户想要使用id为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:lp7200
权限,但是用户没有所有打印机的权限,因此这个代码会阻止用户打印,这是个不正确的权限检查。因此,在写权限检查的代码时,应该进行最小权限的检查
Implication, not Equality
在进行权限检查时,应该尽可能的具体,但是在给用户授权时,应该让权限尽可能的通用一些。权限检查是通过其隐含意义而不是等值比较来进行计算的。比如给用户授权时是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
user:update:12345
# 第一个权限字符串隐含了第二个权限
printer
printer:print
性能
因为权限检查是基于其隐含意义而不是等值比较的,因此每次权限检查时都会去查看赋予给用户的权限是否隐含了我们需要的权限,这是一个复杂的操作,为了提升性能,Shiro会将第一次检查成功的结果缓存起来,以后再次进行相同的检查时会立即返回
Shiro中有一个CacheManager
组件,其负责缓存的相关工作,当CacheManager
将用户、角色和权限缓存在内存中时,这会让权限检查非常快。但是,如果分配给用户或其角色或组的权限数量增加时,执行权限检查的时间也必然会增加
如果应用程序的Realm
可以用更加有效的方法来执行权限检查,那就应该去实现Realm
的isPermitted*
方法,虽然默认的Realm/WildcardPermission
能够满足大部分的需求,但是对于一些有大量权限检查需求的应用来说,它们或许不是最好的解决方案