SpringBoot集成Shiro实现多数据源认证授权与分布式会话(二)

描述

继上一篇文章SpringBoot集成Shiro实现多数据源认证授权与分布式会话(一)接下来我们再来看看shiro如何实现多数据源认证授权,由于在业务上的需要,我们系统提供了app端和pc端两种登录入口,app端又细分为手机号码登录和第三方应用登录两种渠道,再加上pc端后台登录一共有三种不同的认证渠道,用户数据也分别存储在两张不同的表结构中即app用户表和后台用户表,所以系统一共需要AppRealm(手机)和PcRealm(后台)以及ThirdRealm(第三方)三种不同的shiro realm,下面我们结合shiro来看看具体的实现步骤.

实现步骤

在一般情况下shiro默认只有一个realm,所有用户的认证授权都由这个realm来处理,当我们配置了多个realm的时候,shiro在认证时会根据不同的登录渠道调用相应的realm来处理认证操作,而鉴权过程则是根据controller层中的@RequiresPermissions注解来check用户是否有操作权限并在迭代realms集合中的元素时调用相应realm对象中的doGetAuthorizationInfo方法一直到shiro的认证器ModularRealmAuthorizer类中的isPermitted方法返回true为止否则继续迭代到循环体结束,具体可看以下shiro源码org.apache.shiro.authc.pam.ModularRealmAuthorizer类中的isPermitted方法.

1
2
3
4
5
6
7
8
9
10
public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {//getRealms获取到的是realms集合
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;//return true则停止迭代
}
}
return false;
}

所以在这里为了防止权限校验过程中由于频繁调用doGetAuthorizationInfo方法造成不必要的性能消耗以及实现不同的登录渠道调用不同的realm,我们先定义一个枚举类DeviceType来区分不同的登录设备类型.

1
2
3
4
5
6
7
8
9
10
11
public enum DeviceType {
PC("Pc"), APP("App"), THIRDPATH("ThirdPath");
private String type;
DeviceType(String type) {
this.type = type;
}
@Override
public String toString() {
return this.type;
}
}

接下来我们还需要自定义用户/密码身份认证Token类并实现带参的构造方法以便我们可以将区分多端用户的属性传进去,该类扩展继承自shiro的UsernamePasswordToken,大概代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 记录设备类型 用于区分APP或者PC端用户
*/
private String deviceType;
/**
* 记录登录类型 用于区分正常手机号码登录还是第三方应用登录
*/
private Integer fromType;
public CustomizedUsernamePasswordToken(final String userNamefinal String password, Integer fromType, String deviceType) {
super(userName, password);
this.setDeviceType(deviceType);
this.setFromType(fromType);
}
省略get和set方法...

由于shiro默认使用的认证器是org.apache.shiro.authc.pam.ModularRealmAuthorizer,为了实现多realm认证必须自定义我们自己的认证器CustomizedModularRealmAuthenticator并覆写其中的doAuthenticate方法,这样就可以通过登录时传给token类的deviceType与realm数据源名称匹配决定当前认证调用的是哪个数据源,具体代码如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 重写doAuthenticate让APP帐号和PC帐号自动使用各自的Realm
*/
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
/**
* 判断getRealms()是否返回为空
*/
this.assertRealmsConfigured();
/**
* 强制转换回自定义的CustomizedUsernamePasswordToken
*/
CustomizedUsernamePasswordToken customizedToken = (CustomizedUsernamePasswordToken) authenticationToken;
/**
* 登录设备类型
*/
String deviceType = customizedToken.getDeviceType();
/**
* 所有自定义的Realm
*/
Collection<Realm> customerRealms = this.getRealms();
/**
* 登录设备类型对应的所有自定义Realm
*/
Collection<Realm> deviceRealms = new ArrayList<>();
/**
* 这里所有自定义的Realm的Name必须包含相对应的设备名
*/
for (Realm realm : customerRealms) {
if (realm.getName().contains(deviceType))
deviceRealms.add(realm);
}
/**
* 判断是单Realm还是多Realm
*/
if (deviceRealms.size() == 1) {
return doSingleRealmAuthentication(deviceRealms.iterator().next(),
customizedToken);
} else {
return doMultiRealmAuthentication(deviceRealms, customizedToken);
}
}

到这里基本上我们已经完成对多数据源realm认证的底层改造了,下一步实现自定义的realm很简单,只要继承自org.apache.shiro.AuthorizingRealm类并覆写其中的doGetAuthorizationInfo方法和doGetAuthenticationInfo方法就可以了,需要注意的是在认证时我们获取认证信息使用的是自定义的token类CustomizedUsernamePasswordToken而不再是默认的UsernamePasswordToken类,另外在做权限校验时可以通过将用户登录之后存储在session中的deviceType取出来判断这种方式来避免所有的realm都会执行一遍自己的doGetAuthorizationInfo方法的造成的性能消耗问题,如以下代码:

1
2
3
4
5
6
Subject subject = SecurityUtils.getSubject();
String deviceType = (String) subject.getSession().getAttribute(
SessionCons.DEVICE_TYPE);
if (deviceType.equals(DeviceType.APP.toString())) {//true则往下执行
省略其他....
}

最后一步是在配置shiro的安全管理器securityManager时要把实现的多个自定义数据源realm依次添加到realms集合中,再调用securityManager的setRealms方法将集合作为参数传进去.

1
2
3
4
5
6
Collection<Realm> realms = new ArrayList<>();
realms.add(appShiroRealm());
realms.add(pcShiroRealm());
realms.add(thirdPathShiroRealm());
securityManager.setRealms(realms);
其他略...

剩下的就是在controller层实现login操作了,注意要将不同登录渠道的deviceType传到自定义的token中,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 登录结果回写用户信息和token
Map<String, Object> responseDto = new HashMap<>();
// 判断是否已经登录
if (!subject.isAuthenticated()) {
// 私钥解密后的账户密码 shiro用来认证登录
CustomizedUsernamePasswordToken token;
if (isThird) {
//第三方应用使用openId登录
token = new CustomizedUsernamePasswordToken(userAccount,
MD5.md5(userPassword), fromType, DeviceType.THIRDPATH.toString());
} else {
//正常App用户手机号码登录
token = new CustomizedUsernamePasswordToken(userAccount,
MD5.md5(userPassword), fromType, DeviceType.APP.toString());
}
token.setRememberMe(false);
subject.login(token);
subject.getSession().setAttribute(SessionCons.DEVICE_TYPE,DeviceType.APP.toString());
省略其他业务逻辑...
}

到此我们已经完全实现了shiro的多数据源认证授权,下一篇再来看看如何结合redis的使用实现分布式session管理.