浅谈访问控制(2)--Shiro

Apache Shiro是Java的一个安全框架, 其核心功能主要包括认证(Authentication)和访问控制(Authorization). 大体看了一下Shiro的访问控制部分, 发现其实现的功能还是比较清晰和简单的. 本章我们会由浅入深地分析Shiro框架中的访问控制功能, 并在最后结合个人经验给出一些使用建议.

概述

访问控制模型

Shiro框架实现了典型的基于资源的访问控制模型(Resource Based Access Control, The New RBAC), 用户通过角色与权限建立联系. 此外, Shiro还支持用户直接与权限建立联系, 从而实现更加直观的访问控制.

实体

用户

Shiro框架中用Subject类表示用户. 用户可以进行登录, 登出, 权限检查等等操作.

权限

在Shiro框架中, 用Permission类表示权限, 即访问控制中的策略, 通常是一些表示行为许可的语句. 权限中至少需要包含资源(Resources)和动作(Actions). 需要注意, 权限只定义了对某种资源执行某种操作的能力, 并没有将权限授予用户的含义.

角色

Shiro中的角色表示一系列行为和职责的集合, 用Role类表示. 角色会与用户进行绑定, 一个用户可以绑定多个角色. Shiro中包含显式角色和隐式角色, 我们完全可以只使用显式角色, 并把角色认为是一系列Permission的集合.

需要注意, Shiro中的角色与IAM, RAM等主流云平台的角色概念并不相同, 而是类似于组的概念.

关系

要想使用Shiro进行访问控制, 首先需要为用户赋予权限, 然后在用户执行各种操作时, 判断已经授予的权限中是否包含执行该操作的权限, 如果包含, 则允许访问, 否则拒绝访问. 另一方面, 用户还可以绑定角色, 执行角色所包含的操作. 因此就存在着用户-权限, 用户-角色, 角色-权限这三种关系. 需要注意, 用户绑定权限和用户绑定角色是可以共存的, 用户也可以同时绑定多个角色, 同时拥有这些角色的权限.

用户-权限

通过为用户绑定一系列的权限, 直接允许用户执行某些操作. 这种方式优点是简单直观, 用户拥有的权限一目了然; 缺点是管理成本较高, 设想如果需要对某种同类型的用户批量管理权限, 则需要依次对每个用户执行绑定操作.

用户-角色 + 角色-权限

创建某种角色, 并为这种角色绑定权限, 该角色就有了操作某种资源的权力. 再将用户绑定到该角色, 则用户也会同时拥有角色的权限. 使用角色进行权限管理的优势在于便于对用户进行分类管理, 缺点在于需要维护两种关系, 无形中增加了维护成本(设想, 如果删除一个角色, 需要同时删除用户-角色的关系和角色-权限的关系).

权限语法

Shiro的权限语法可以说是完全开放的, 开发者可以任意实现权限的语法并提供对应的解析器. Shiro默认提供了一种”通配符权限”语法规则, 并实现了一个解析器. 通配符权限语法是极其简单的, 仅支持3种符号: *, :, ,.

  • * 用于通用字符串匹配.
  • : 用于分隔不同字段.
  • , 用于分隔相同字段中的多个值.

举个例子, 定义一个格式为resource:actions的权限, 表示”允许对某个资源进行某些操作的权力”. 示例中定义了两个action: getBucket和deleteBucket, 允许操作名为myBucket的资源.

1
2
# resource:actions
myBucket:getBucket,deleteBucket

注意, 上面的resource:actions是我自己定义的, 并不是Shiro官方定义的. 只要符合通配符权限的语法规则, 你完全可以实现任意的权限语义.

使用方法

Shiro框架提供了两种鉴权方式: 基于权限鉴权和基于角色鉴权; 并提供了3种使用方式: 编程式, 注解式, JSP TagLib (不作介绍).

鉴权流程:

  • 1 SecurityUtils.getSubject()获取当前线程的Subject对象(实际上是DelegatingSubject代理对象)
  • 2 UsernamePasswordToken token = new UsernamePasswordToken(username, password); 创建用户名密码token
  • 3 subject.login(token); 如果成功登录, 则通过认证, 否则抛出AuthenticationException异常.
  • 4 对subject对象进行鉴权

鉴权方式

鉴权方式包括基于权限鉴权基于角色鉴权, 具体细节在上下文中已有说明, 在此略过.

使用方式

编程式

调用Subject的鉴权相关方法进行鉴权, 通过返回值或者断言来判断鉴权是否成功, 具体可参考Shiro文档.

注解式

Shiro提供了2个权限相关的注解: @RequiresPermissions, @RequiresRoles.

  • @RequiresPermissions 判断用户是否绑定指定权限, 参数为权限名称列表, 支持AND和OR的连接方式.
  • @RequiresRoles 判断用户是否绑定指定角色, 参数为角色名称列表, 支持AND和OR的连接方式.

源码分析

组件

跟开涛学Shiro博文中有相关的组件介绍, 在这里给出一些个人理解.

Permission

Permission表示执行某项操作或者访问某个资源的权力. 需要注意的是, Permission对象仅表示权力属性, 不表示授权行为.

Authorizer

Authorizer接口声明了各种用于鉴权的方法, 主要分为4类:

  • checkRole系列: 检查是否包含角色, 不包含则抛出AuthorizationException异常.
  • checkPermission系列: 检查是否包含权限, 不包含则抛出AuthorizationException异常.
  • hasRole系列: 检查是否包含角色, 不包含则返回false.
  • isPermitted系列: 检查是否包含权限, 不包含则返回false.

Realm

文档给出的Realm的定义是:获取应用特定的安全实体(如user, role, permission)的组件, 以确定认证和鉴权操作. Realm通常与数据源是一一对应的关系. 默认情况下, Realm的实现会同时提供认证和鉴权的功能, 如果你不希望提供认证功能, 需要覆写supports(AuthenticationToken)方法, 使其始终返回false.

Realm接口声明了一个getAuthenticationInfo(AuthenticationToken)方法, 根据AuthenticationToken获取用户信息.

Subject

Subject对象表示用户的状态和安全操作信息.

SecurityManager

Shiro框架中安全管理的大管家接口, 是Shiro框架的核心, 所有与安全有关的操作都会与SecurityManager交互, 并管理着所有Subject对象. 该接口继承了Authenticator, Authorizer, SessionManager这三个接口, 并声明了以下方法:

1
2
3
4
5
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
void logout(Subject subject);
Subject createSubject(SubjectContext context);
}

核心鉴权逻辑

Shiro的核心鉴权逻辑位于Permission接口的implies(Permission)方法中. 该方法是一个实例方法, 对某个Permission对象a调用implies()并传入另一个Permission对象b, 如果a的权限大于b, 则返回true, 否则返回false.

1
2
3
public interface Permission {
boolean implies(Permission p);
}

由此可以猜测一下Shiro的鉴权逻辑: 鉴权操作传入一个Permission对象, Shiro根据当前用户信息查询用户绑定的Permission对象集合, 对集合中的每个Permission调用implies()方法, 只要有任何一个返回true, 即表示鉴权通过, 否则返回false表示鉴权不通过. 至于是不是这样, 我们继续看源码.

来看一看delegatingSubject.isPermitted(String permission)的调用流程.

首先DelegatingSubject对象会委托内部的大管家对象securityManager调用isPermitted(getPrincipals(), permission), securityManager进一步委托内部authorizer(默认是ModularRealmAuthorizer)调用isPermitted(principals, permissionString). 在默认的Authorizer实现类ModularRealmAuthorizer中, isPermitted最终会调用Permission的implies()方法进行鉴权.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ModularRealmAuthorizer implements Authorizer,
PermissionResolverAware, RolePermissionResolverAware {
...
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
assertRealmsConfigured();
// 遍历所有的Realm对象, 如果Realm实现了Authorizer鉴权接口, 则调用其isPermitted()方法进行鉴权
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;
}
}
return false;
}
...

最内的一层调用用户定义的realm对象的isPermitted()方法, 一般情况下我们会继承AuthorizingRealm来实现Authorizer接口, AuthorizingRealm的isPermitted()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public abstract class AuthorizingRealm extends AuthenticatingRealm
implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
...
// 需要调用入参为Permission的重载方法
public boolean isPermitted(PrincipalCollection principals, String permission) {
Permission p = getPermissionResolver().resolvePermission(permission);
return isPermitted(principals, p);
}
// 转换成AuthorizationInfo, 调用重载方法
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permission, info);
}
// 用AuthorizationInfo中的每个Permission对传入的Permission鉴权
private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
...
}

对于比较常用的WildcardPermission来说, 其implies()方法如下所示. 需要注意的是, Shiro的通配符匹配规则有一定的限制, 仅支持完全匹配(即*包含所有), 不支持前后缀匹配(即不支持get*,*Bucket这样的匹配).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class WildcardPermission implements Permission, Serializable {
...
public boolean implies(Permission p) {
if (!(p instanceof WildcardPermission)) {
return false;
}
WildcardPermission wp = (WildcardPermission) p;
List<Set<String>> otherParts = wp.getParts();
int i = 0;
for (Set<String> otherPart : otherParts) {
if (getParts().size() - 1 < i) { // 如果当前权限比传入的权限缺少最后n个字段, 则鉴权通过
return true;
} else { // 按照通配符完全匹配规则比较每个字段
Set<String> part = getParts().get(i);
if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) {
return false;
}
i++;
}
}
}
...
}

如果鉴权通过, 即继续执行权限对应的功能代码; 如果鉴权失败, 程序应该抛出异常或返回.

总结

Shiro框架实现了基于资源的访问控制模型, 其本身不包含访问控制规则, 而仅仅提供了一个访问控制的框架. 通过使用权限, 实体, 角色, Realm等组件, 用户可以自定义一套权限集合, 并提供鉴权规则, 从而实现不同粒度的访问控制.

优劣

Shiro框架的访问控制功能是极其灵活的, 只要实现Permission接口, 即可随意定义权限规则. 然而, 其自带的权限实现的功能极其有限, 通配符权限WildcardPermission仅支持完全匹配, DomainPermission更是只有domain, actions, targets这三个字段, 难以满足复杂的权限管理.

参考资料