Apache Shiro 授权
授权,也就是访问控制
,换句话说,控制谁
可以访问什么
,举个例子,是否允许用户访问某个网页,修改某个数据等,这取决于用户可以访问什么资源
授权相关的核心概念
在Shiro中,授权有三个核心的观念:权限,角色,用户
权限
在Shiro中,权限代表了安全策略中最基本的元素,它们显式地表示在应用程序中可以做哪些事情,一个格式良好的权限语句,应该是描述了某个资源,以及当主体(Subject
)与这些资源交互时可能发生的操作,例如以下权限语句
- 打开一个文件
- 浏览 ”/user/list" web 页面
- 打印文档
- 删除 "jsmith" 用户
大多数资源都应该会支持CRUD
操作,任何对特定资源类型有意义的操作都是可以的,所以权限语句至少是基于资源和操作的,因此,在查看权限时,需要意识到权限没有表示谁可以执行所表示的行为,它们只是表示了在应用程序中可以做什么
权限仅仅代表行为,它们只是反映了对特定资源类型的操作,并不表示谁可以执行这些操作
允许谁(用户)做什么(权限)即为授权,这应该取决于你的应用程序的数据模型,不同的应用程序会有很大的不同
比如可以将多个权限集合起来可以组成一个角色,角色可以赋予给多个用户,这样被赋予角色的用户就拥有了该角色中的所有权限;也可以把多个用户组成一个用户组,给这个用户组赋予角色,该组中的所有用户都被隐式地授予了角色中的权限。对于如何将权限授予用户有许多不同的方式,应该根据需求确定如何对此进行建模
权限粒度
多数情况下,对于权限的粒度通常划分到资源类型级别,比如对文件类型可以进行某些操作,但是在某些情况下,也可以进行更细粒度的实例级别的划分,例如对某个文件具有某个操作的权限。Shiro支持这种实例级别的权限
角色
角色可以认为是一组权限的集合,可以给用户赋予角色,这样用户就拥有了角色中的所有权限。Shiro支持两种类型的角色:
-
隐式角色:大多数人使用角色来进行隐式授权,也就是说,通过检查用户是否有某一角色来判断他是否有某一个权限
使用隐式角色会对软件的维护和管理造成巨大的影响。例如,如果你想删除或添加某一个角色,这需要修改源代码,重新编译,测试,上线,每次需要做出修改时都需要这样。所以隐式角色适用于简单应用,比如那种只有管理员和非管理员两种用户,但是对于更复杂的可配置的应用,就会有大问题
显式角色:显式角色就是在授权时通过角色给用户授权,但是在权限检查时仍然通过权限进行检查
Shiro团队提倡在开发中最好采用显式角色的方式,RBAC
即 Resource-Based Access Control
,基于资源的访问控制
用户
在Shiro中,用户即Subject
,这里我们用用户来指代Subject
通过向用户赋予角色或直接授予权限,允许用户在应用程序中执行某些操作,应用程序数据模型精确地定义了允许用户做那些事。举个例子,在你的数据模型中,有一个User
类,你可能将权限直接赋予给User
实例,或者将多个权限组合成Roles
,然后给User
赋予Role
,这样User
也有了这些Role
的权限,再或者你将多个用户放到一个“用户组
”中,给这个用户组授权来达到间接授权的效果
应用程序的数据模型准确地定义了授权会如何起作用。Shiro依赖Realm
将数据模型关联细节转换为Shiro能够理解的格式
Realm最后将会和数据源连接,它将会告诉Shiro是否存在某个角色或者权限
授权中的Subject
在Shiro中进行权限检查有3种方式:
- 编程式:直接使用Java代码来进行检查
- JDK注解:可以在Java方法上添加注解来进行检查
- JSP/GSP 标签库:可以基于角色和权限来控制JSP或GSP页面的输出内容
编程式权限检查
最简单且最通用的方式就是,直接通过Subject
实例的API进行编程
基于角色的权限检查
如果希望通过传统的基于角色名字的隐式检查来进行访问控制,可以进行基于角色的检查
通过hasRole*
方法
通过Subject
实例的hasRole*
方法,可以检查当前的Subject
是否有某一个角色
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("administrator")) {
//show the admin button
} else {
//don't show the button? Grey it out?
}
下面是Subject
中关于角色检测的方法
方法 | 描述 |
---|---|
hasRole(String rolename) | 如果Subject有指定的角色,则返回true,反之返回false |
hasRoles(List<String> roleName) | 同理,只要给定角色列表中其中一个,则返回true |
hasAllRoles(Collection<String> roleNames) | Subject必须有给定列表所有的角色才会返回true |
采用断言
另一种判断Subject是否有某个角色的方法是通过断言的方式,如果这个Subject
没有指定的角色名,则会抛出一个AuthorizationException
,反正,代码正常进行
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is a bank teller and
//therefore allowed to open the account:
currentUser.checkRole("bankTeller");
openBankAccount();
这种方式相比于hasRole*
方法的优点在于,代码可以更加简洁,在Subject没有需要的角色时不需要构造自己的AuthorizationException
。下面时这种方式相关的方法
方法 | 描述 |
---|---|
checkRole(String roleName) | 检查是否有某个角色,否则抛异常 |
checkRoles(Collection<String> roleNames) | 检查是否有所有的角色,否则抛异常 |
checkRoles(String... roleNames) | 与上面的方法相同 |
基于权限的检查
如前所述,在进行访问控制时,最好是基于权限的,因为基于权限的检测是和应用程序的功能相关的,只有在应用程序功能改变而非安全策略改变时,才需要改变相关权限检查代码,相比于基于角色检查的代码,这种方式受到安全策略变化的影响较小
通过isPermitted*
系列方法
如果想要检查一个Subject
是否有做某个操作的权限,可以调用各种isPermitted*
的方法。该方法可以传入两种参数,一种是基于对象的Permission
实例,一种是用字符串代表的权限
基于对象的权限检查
可以用实现了Shiro的org.apache.shiro.authz.Permission
接口的实例来代表一个权限,将其传给isPermitted*
方法来进行权限检查。考虑如下场景,办公室有一台打印机(资源类型为printer
),其在软件中的唯一标识为laserjet4400n
,在打印文档之前,我们需要检查当前用户是否有print
的权限来决定是否给用户显示打印的按钮,其相关代码如下:
Permission printPermission = new PrinterPermission("laserjet4400n", "print");
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted(printPermission)) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
上面的例子,也是一个实例级访问控制检查的例子
在下面所述的场景中,基于对象的权限是非常有用的:
- 需要在编译时检查类型安全
- 需要确保权限被正确的表示和使用
- 需要显式的控制权限的解析逻辑的执行方式(基于
permission
接口implies
方法,称为权限蕴涵逻辑) - 需要确保权限精确的反映的应用程序资源,例如,
Permission
类可以在基于项目域模型的项目构建过程中自动生成
以下是Subject
中的相关方法:
方法 | 描述 |
---|---|
isPermitted(Permission p) | 如果有权限则返回true |
isPermitted(List<Permission> perms) | 只要有perms中任何一个权限,返回true |
isPermitted(Collection<Permission> perms) | 如果有全部perms中的权限才返回true |
基于字符串形式的权限检查
虽然基于对象的权限表示方式有很多优点(编译时类型检查,确保行为正确,自定义权限解析逻辑等),但大多数情况下这是一种比较重量级的方式,另一种方式是采用字符串String
来代表一个权限
对于上面的例子,通过字符串表示权限,则为如下代码:
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted("printer:print:laserjet4400n")) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
这个例子也展现了实例级别的权限检查,更重要的是权限的三部分——printer
(资源类型),print
(操作)和laserjet4400n
(实例ID),它们都是以字符串的形式表示的
上面这种冒号风格的权限字符串,是由Shiro的org.apache.shiro.authz.permission.WildcardPermission
定义的,之后由它来解析权限字符串。所以权限字符串本质上也是前面的基于对象的其中一种方式
Subject currentUser = SecurityUtils.getSubject();
Permission p = new WildcardPermission("printer:print:laserjet4400n");
if (currentUser.isPermitted(p) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
上面的字符串形式,默认使用WildcardPermission
的格式,当然也可以自己定义字符串的格式,并在项目中使用它,这一部分会在后面讲解 Realm 授权的小节中讲到
Shiro这种默认的基于字符串形式的权限表示方式,让开发人员不需要关心如何实现接口,一个简单的字符串就可以表示一个权限,缺点则是没有类型安全检查,而且难以适应更复杂的行为。如果需要的行为难以用默认的权限字符串语法表示时,就需要基于Shiro的Permission
接口自定义permission
类型。实际上,大多数用户都会选择Shiro默认的基于字符串的权限表示方式,因为它足够简单。当然,最后采用那种方式还是取决于项目需要
Subject对于这种方式提供了如下支持:
方法 | 描述 |
---|---|
isPermitted(String perm) | 如果有权限perm,则返回true |
isPermitted(String.. perms) | 只要有perms其中一个权限,则返回true |
isPermittedAll(String... perms) | 需要有perms所有的权限,才返回true |
采用断言
另一种检查权限的方式则是采用断言的方式,如果当前Subject没有指定的权限,则会抛出一个AuthorizationException
,如果有指定的权限,则会继续执行后面的代码
如下代码
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is permitted
//to open a bank account:
Permission p = new AccountPermission("open");
currentUser.checkPermission(p);
openBankAccount();
也可以采用权限字符串
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is permitted
//to open a bank account:
currentUser.checkPermission("account:open");
openBankAccount();
这种方法相比于isPermitted*
方法的好处就在于代码更加清晰,如果当前Subject没有相关权限时,也不需要构造自己的AuthorizationException
以下是Subject支持的断言方法
方法 | 描述 |
---|---|
checkPermission(Permission p) | 断言是否有指定权限,否则抛出异常 |
checkPermission(String perm) | 同上,但采用权限字符串 |
checkPermissions(Collection<Permission> perms) | 断言是否有所有的指定权限,否则抛出异常 |
checkPermissions(String... perms) | 同上,但采用权限字符串 |
基于注解的权限检查
除了Subject的API以外,Shiro也支持Java中的注解来进行权限控制
配置
在使用Java注解之前,需要先在应用程序中开启AOP的支持,虽然有各种不同的AOP框架,但没有一种标准的方式可以在所有的应用程序中开启AOP支持,以下是常用的项目类型如何开启AOP
对于AspectJ,可以参考样本项目
对于Spring项目,查看Spring集成Shiro相关文档
对于Guice项目,参考Guice集成Shiro相关文档
RequiresAuthentication
注解
RequiresAuthentication注解可以标注在类,接口和方法上,表示需要当前的Subject在当前的会话中需要是已认证才可以访问其中的方法,如下
@RequiresAuthentication
public void updateAccount(Account userAccount) {
//this method will only be invoked by a
//Subject that is guaranteed authenticated
...
}
上面的代码和下面是等效的
public void updateAccount(Account userAccount) {
if (!SecurityUtils.getSubject().isAuthenticated()) {
throw new AuthorizationException(...);
}
//Subject is guaranteed authenticated here
...
}
RequireGuest
注解
RequireGuest
注解可以标注在类,接口和方法上,表示需要当前的Subject需要是未认证和未记住的状态才可以调用这些方法,如下代码
@RequiresGuest
public void signUp(User newUser) {
//this method will only be invoked by a
//Subject that is unknown/anonymous
...
}
上面的代码和下面是等效的
public void signUp(User newUser) {
Subject currentUser = SecurityUtils.getSubject();
PrincipalCollection principals = currentUser.getPrincipals();
if (principals != null && !principals.isEmpty()) {
//known identity - not a guest:
throw new AuthorizationException(...);
}
//Subject is guaranteed to be a 'guest' here
...
}
RequiresPermisssions
注解
RequiresPermissions
注解标注在类,接口和方法,表示需要当前Subject拥有被指定的权限字符串表示的权限才可以执行被标注的方法,或类中的方法。其中RequiresPermissions
可以接收多个参数,默认情况下需要同时拥有这些字符串表示的权限才可以调用被标注的方法,通过改变其中的logical
值可以修改这个默认行为
例子如下:
@RequiresPermissions("account:create")
public void createAccount(Account account) {
//this method will only be invoked by a Subject
//that is permitted to create an account
...
}
上面的代码和下面的等同
public void createAccount(Account account) {
Subject currentUser = SecurityUtils.getSubject();
if (!subject.isPermitted("account:create")) {
throw new AuthorizationException(...);
}
//Subject is guaranteed to be permitted here
...
}
RequiresRoles
注解
RequireRoles
注解和RequiresPermissions
基本相同,也是标注到类,接口和方法上的,但它接收的参数是角色名字。如果当前Subject
没有指定的角色,则会抛出AuthorizationException
异常
如下
@RequiresRoles("administrator")
public void deleteUser(User user) {
//this method will only be invoked by an administrator
...
}
上面的代码和下面的等价
public void deleteUser(User user) {
Subject currentUser = SecurityUtils.getSubject();
if (!subject.hasRole("administrator")) {
throw new AuthorizationException(...);
}
//Subject is guaranteed to be an 'administrator' here
...
}
RequiresUser
注解
RequiresUser
注解可以标注到类,接口和方法上,表示需要当前的Subject是已认证或者已记住的状态才可以调用方法,正好和RequireGuest
注解的作用相反
例子如下:
@RequiresUser
public void updateAccount(Account account) {
//this method will only be invoked by a 'user'
//i.e. a Subject with a known identity
...
}
上面的代码和下面的等效
public void updateAccount(Account account) {
Subject currentUser = SecurityUtils.getSubject();
PrincipalCollection principals = currentUser.getPrincipals();
if (principals == null || principals.isEmpty()) {
//no identity - they're anonymous, not allowed:
throw new AuthorizationException(...);
}
//Subject is guaranteed to have a known identity here
...
}
JSP 标签库
这一部分会在Web章节中详细说明
授权流程
现在我们来看看一个授权操作在Shiro中是如何完成的,首先看下图
-
第一步:应用程序代码调用
Subject
的hasRole*
,checkRole*
,isPermitted*
,checkPermission*
方法,传入所需的权限和角色 -
第二步:
Subject
,一般是DelegatingSubject
或其子类,将工作委托给SecurityManager
,通过调用securityManager
的hasRole*
,checkRole*
,isPermitted*
,checkPermission*
方法,因为SecurityManager
实现了org.apache.shiro.authz.Authorizer
接口,该接口定义了上面描述的关于权限的所有方法(hasRole*
,checkRole*
,isPermitted*
,checkPermission*
) -
第三步:
SecurityManager
委托它内部的org.apache.shiro.authz.Authorizer
组件,调用authorizer
组件的hasRole*
,checkRole*
,isPermitted*
,checkPermission*
方法进行权限检查,该authorizer
组件默认是ModularRealmAuthorizer
的实例,它可以协调多个Realm
一起完成访问控制的工作 -
第四步:挨个检查应用程序配置的
Realm
是否实现了Authorizer
接口,如果实现了,则直接调用Realm
的hasRole*
,checkRole*
,isPermitted*
,checkPermission*
方法,如果没有,则忽略
ModularRealmAuthorizer
如前所述,Shiro的SecurityManager
默认使用的是ModularRealmAuthorizer
实例,ModularRealmAuthorizer
即支持单Realm应用,也支持多Realm应用
ModularRealmAuthorizer
在进行访问控制时,会遍历其内部的Realm集合,并对每一个Realm进行如下操作:
-
如果
Realm
实现了Authorizer
接口,则会调用其中的Authorizer
接口定义的方法(hasRole*
,checkRole*
,isPermitted*
,checkPermission*
)如果调用
Realm
实现的方法抛出了异常,则这个异常将会被封装为AuthorizationException
返回给最上层的Subject
调用者,同时中止本次权限检查的流程,不会继续去遍历剩余的Realm
-
如果调用的是Realm的
hasRole*
或者isPermitted*
方法并且返回值为true
,那将会立即返回true
,剩下的Realm
将不再被遍历。此举是为了保证性能,如果其中一个Realm
允许这次操作,那就说明这次操作是被允许的,也就没必要继续遍历剩下的Realm
典型的安全策略是采用白名单的方式,即法无授权即禁止
如果
Realm
没有实现Authorizer
接口,则会被忽略
Realm 的顺序
和认证一样,ModularRealmAuthorizer
与Realm
交互顺序是按照其迭代顺序来的,ModularRealmAuthorizer
将会按照一定的顺序遍历Realm
,如果Realm
实现了Authorizer
接口,则调用Authorizer
中定义的方法(hasRole*
,checkRole*
,isPermitted*
,checkPermission*
),有关改变顺序的方法和认证是一样的
PermissionResolver
默认情况下,当使用基于权限字符串
时,Shiro会首先将该字符串转换成一个Permission
实例。这是因为Permissions
是基于隐含意义而不是直接相等来计算的(详情查看后面的Permission
文档),大多数Realm
需要将提交的权限字符串转换或解析为相应的具有代表性的permission
实例
为了进行这个转换,Shiro提出了PermissionResolver
的概念,大多数的Shiro Realm都会使用一个PermissionResolver
来将方法参数中的权限字符串转换成Permission
实例。Shiro默认会按照WildcardPermission
的格式,使用WildcardPermissionResolver
将字符串解析成permission
实例
如果想要自定义权限字符串语法,则也需要实现相应的PermissionResolver
进行解析,并将其设置为全局解析器,如下配置
globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...
如果你想要配置一个全局的
PermissionResolver
,每个接收配置的PermissionResolver
的Realm
必须实现PermissionResolverAware
接口。这保证了配置的Realm
实例可以使用新配置的PermissionResolver
如果你不想你的Realm
实现PermissionResolverAware
,也可以为Realm
直接配置指定的PermisssionResolver
permissionResolver = com.foo.bar.authz.MyPermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...
RolePermissionResolver
和PermissionResolver
类似,RolePermissionResolver
也是用来将字符串转换为需要的Permission
实例的,不同的是,传入RolePermissionResolver
的是角色名,而不是权限字符串
它的应用场景适用于某些数据源不支持权限的概念时,比如,在LDAP中存储角色的名字,而没有存储角色对应的权限,因为LADP不支持权限的概念,RolePermissionResolver
就可以将角色名转化为具体的权限,而这些具体的权限会被存储在其他数据源中,如本地数据库
但是上述场景比较少见,因此Shiro内置的Realm
并没有使用它们。可以通过下面的配置,为所有的Realm
指定一个全局的自定义RolePermissionResolver
globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...
和
PermissionResolver
一样,Realm
必需实现RolePermissionResolverAware
才会使用配置的RolePermissionResolver
如果不想使用全局的RolePermissionResolver
,也可以为Realm
指定特定的RolePermissionResolver
自定义Authorizer
如果默认的ModularRealmAuthorizer
不能满足你的应用需求,也可以实现自己的Authorizer
来进行访问控制,只需要实现Authorizer
接口,并在SecurityManager
指定即可
[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer
securityManager.authorizer = $authorizer