如何通过diboot实现shiro的无状态实践
diboot iam-base 是一款基于 shiro 安全框架二次开发的应用于 PC WEB 的前后端分离的认证授权框架,所以服务端的认证信息依然使用有状态管理,即 session 存储. 但是…
一、前言
diboot iam-base 是一款基于 shiro 安全框架二次开发的应用于 PC WEB 的前后端分离的认证授权框架,所以服务端的认证信息依然使用有状态管理,即 session 存储.
但是最近发现有的小伙伴将 diboot-iam 用在移动端,我们知道移动端是没有 session,此时我们就需要对 diboot-iam 进行一点微小的改造,让其进入无状态管理,从而适应于移动端认证。
注:本课程基于 diboot2.1.2 版本
diboot2.2.0 后无状态已经内置 StatelessJwtAuthFilter,直接使用即可
二、你可以学到?
- shiro 无状态改写
- 如何基于 iam-base 的扩展其他登录方式
- 如何替换框架中默认的 iam_user 用户类型
三、代码
业务场景:小程序登陆,改写原始的用户名密码登陆,基于微信用户的 openid,无密登陆
3.1 无状态配置
基于 diboot-iam 的 DefaultJwtAuthFilter, 创建新的 shiro 过滤器,并进行 token 相关的调整
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117/**
* 认证过滤器
* @author : uu
* @version : v1.0
* @Date 2020/10/27 19:34
*/
public class StatelessJwtAuthFilter extends DefaultJwtAuthFilter {
/**
* 判断是否登录
* 这里逻辑只是进行简单的修改,增加了refreshtoken(这里我设置生成逻辑和token生成逻辑内容相同,但是该token永久有效),可以选择其他方式
* <br/>
* 大致逻辑为:token没过期,使用token;
* token即将过期,使用token换新的token;
* token已经过期,使用refreshtoken换新的token
* 最后拿到可用的token,进行登陆(登陆这里还是使用原来的realname,可以查看BaseJwtRealm逻辑,根据需要使用缓存登陆,或者直接查询数据库)
*
* 返回true则直接进入控制器
* @param request
* @param response
* @param mappedValue
* @return
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
// OPTIONS类型的请求需要拦截处理(否则抛出异常),让系统执行真正的请求
if (HttpMethod.OPTIONS.matches(httpRequest.getMethod())) {
return true;
}
// 设置当前token
String currentToken = JwtUtils.getRequestToken(httpRequest);
if (V.isEmpty(currentToken)) {
// 当前token验证失败
return false;
}
boolean needRefreshToken = false;
Claims claims = JwtUtils.getClaimsFromRequest(httpRequest);
// 尝试使用refreshClaims替换
if (V.isEmpty(claims)) {
claims = getClaimsFromRequestByRefreshtoken(httpRequest);
needRefreshToken = true;
}
if (V.isEmpty(claims)) {
return false;
} else if (V.notEmpty(claims.getSubject())) {
// 判断是否需要借助refreshtoken 置换token
if (needRefreshToken) {
log.debug("refreshToken验证成功!account={}", claims.getSubject());
currentToken = JwtUtils.generateToken(claims.getSubject(), (long)JwtUtils.EXPIRES_IN_MINUTES);
JwtUtils.addTokenToResponseHeader((HttpServletResponse)response, currentToken);
} else {
log.debug("Token验证成功!account={}", claims.getSubject());
// 如果当前token有效,如果在指定时间内可以通过当前token进行置换新的token
String newToken = JwtUtils.generateNewTokenIfRequired(claims);
if (V.notEmpty(newToken)) {
JwtUtils.addTokenToResponseHeader((HttpServletResponse)response, currentToken);
currentToken = newToken;
}
}
// 构建登陆的token
BaseJwtAuthToken baseJwtAuthToken = new BaseJwtAuthToken();
baseJwtAuthToken.setAuthAccount(S.substringBefore(claims.getSubject(), ","));
baseJwtAuthToken.setAuthtoken(currentToken);
baseJwtAuthToken.setAuthType(Cons.DICTCODE_AUTH_TYPE.WX_MP.name());
baseJwtAuthToken.setUserTypeClass(WxMember.class);
IamSecurityUtils.getSubject().login(baseJwtAuthToken);
return true;
} else {
log.debug("Token验证失败!");
return false;
}
}
/**
* 没有登录的情况下会走此方法
* @param request
* @param response
* @return
* @throws Exception
*/
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
log.debug("Token认证失败: onAccessDenied");
JsonResult jsonResult = new JsonResult(Status.FAIL_INVALID_TOKEN);
this.responseJson((HttpServletResponse)response, jsonResult);
return false;
}
/**
* 获取refreshtoken的Claims
* @param request
* @return
*/
private Claims getClaimsFromRequestByRefreshtoken(HttpServletRequest request) {
String authHeader = request.getHeader("refreshtoken");
String refreshtoken = null;
if (authHeader != null) {
refreshtoken = authHeader.startsWith("Bearer ") ? authHeader.substring("Bearer ".length()) : authHeader.trim();
}
if (V.isEmpty(refreshtoken)) {
log.warn("refreshtoken 为空!url={}", request.getRequestURL());
return null;
} else {
try {
return (Claims) Jwts.parser().setSigningKey(JwtUtils.SIGN_KEY).parseClaimsJws(refreshtoken).getBody();
} catch (ExpiredJwtException var3) {
log.info("refreshtoken已过期:{}", refreshtoken);
} catch (Exception var4) {
log.warn("refreshtoken解析异常", var4);
}
return null;
}
}禁用 shiro 的 session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**java
* 禁用session
*
* @author : uu
* @version : v1.0
* @Date 2020/10/28 11:06
*/
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}增加 ShiroConfig.java,重新改写 diboot-iam 的 ShiroFilterFactoryBean 定义(此处可以直接复制 IamBaseAutoConfig#shiroFilterFactoryBean 来改动哦)
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69/**
* 无状态shiro配置
*
* @author : uu
* @version : v1.0
* @Date 2020/10/27 19:33
*/
public class StatelessShiroConfig {
private IamBaseProperties iamBaseProperties;
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = new LinkedHashMap();
// ---------更改部分 start-------------
// 设置无状态的过滤器 详细见:StatelessJwtAuthFilter,替换 diboot提供的 DefaultJwtAuthFilter
filters.put("jwt", new StatelessJwtAuthFilter());
shiroFilterFactoryBean.setFilters(filters);
// 设置无状态session,SessionsSecurityManager默认使用DefaultWebSecurityManager实现,这里需要设置session不创建 见自定义:StatelessDefaultSubjectFactory
if (securityManager instanceof DefaultWebSecurityManager) {
DefaultWebSecurityManager defaultWebSecurityManager = ((DefaultWebSecurityManager) securityManager);
// 设置不创建session
defaultWebSecurityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());
// subject禁止存储到session(这里不设置,最后还是会将subject存储)
//详情见org.apache.shiro.mgt.DefaultSubjectDAO#save
DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator();
webEvalutator.setSessionStorageEnabled(false);
((DefaultSubjectDAO)defaultWebSecurityManager.getSubjectDAO())
.setSessionStorageEvaluator(webEvalutator);
}
// ---------更改部分 end-------------
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/diboot/**", "anon");
filterChainDefinitionMap.put("/error/**", "anon");
filterChainDefinitionMap.put("/auth/**", "anon");
filterChainDefinitionMap.put("/uploadFile/download/*/image", "anon");
boolean allAnon = false;
String anonUrls = this.iamBaseProperties.getAnonUrls();
if (V.notEmpty(anonUrls)) {
String[] var7 = anonUrls.split(",");
int var8 = var7.length;
for(int var9 = 0; var9 < var8; ++var9) {
String url = var7[var9];
filterChainDefinitionMap.put(url, "anon");
if (url.equals("/**")) {
allAnon = true;
}
}
}
filterChainDefinitionMap.put("/login", "authc");
filterChainDefinitionMap.put("/logout", "logout");
if (allAnon && !this.iamBaseProperties.isEnablePermissionCheck()) {
filterChainDefinitionMap.put("/**", "anon");
} else {
filterChainDefinitionMap.put("/**", "jwt");
}
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}至此,我们已经将无状态的 shiro 改写完毕。接下来我们再看看如何通过 iam-base 自定义用户登陆
3.2 自定义登陆和替换用户类型
注:此处不会涉及小程序前端代码
iam-base 默认采用 iam_user 为登陆用户的信息表,登陆方式为:用户名 - 密码,如果上述无法满足你的项目需求,你可以看这里自定义扩展替换用户为 wx_member(微信用户的基本信息表)
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/**
* 微信登陆凭证(只需要openid登陆,不需要密码)
*
* 继承AuthCredential
* @author : uu
* @version : v1.0
* @Date 2020/10/27 17:38
*/
public class WXMemberCredential extends AuthCredential {
private static final long serialVersionUID = -738269438230072612L;
/**
* 微信登陆的标示
*/
private String openid;
/**
* 用户类型
*/
private Class userTypeClass = WxMember.class;
public String getAuthAccount() {
return this.openid;
}
public String getAuthSecret() {
return null;
}
}实现 AuthService 接口,定义认证方式及接口实现 (默认提供了一些枚举 Cons.DICTCODE_AUTH_TYPE,不够用可以自定义)
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110/**
* 自定微信登陆service
*
* @author : uu
* @version : v1.0
* @Date 2020/10/27 19:21
*/
public class WXMpAuthServiceImpl implements AuthService {
private IamAccountService accountService;
private HttpServletRequest request;
private IamLoginTraceService iamLoginTraceService;
public String getAuthType() {
return Cons.DICTCODE_AUTH_TYPE.WX_MP.name();
}
public IamAccount getAccount(BaseJwtAuthToken jwtToken) throws AuthenticationException {
// 这里我加了一个本地缓存,先从缓存查询账户是否存在,如果已经存在,那么直接返回
IamAccount cacheAccount = AccountMemoryCache.get(jwtToken.getAuthAccount());
if (V.notEmpty(cacheAccount)) {
return cacheAccount;
}
LambdaQueryWrapper<IamAccount> queryWrapper = Wrappers.<IamAccount>lambdaQuery()
.eq(IamAccount::getUserType, jwtToken.getUserType())
.eq(IamAccount::getAuthType, Cons.DICTCODE_AUTH_TYPE.WX_MP.name())
.eq(IamAccount::getAuthAccount, jwtToken.getAuthAccount())
.orderByDesc(BaseEntity::getId);
IamAccount latestAccount = this.accountService.getSingleEntity(queryWrapper);
if (latestAccount == null) {
return null;
} else if (Cons.DICTCODE_ACCOUNT_STATUS.I.name().equals(latestAccount.getStatus())) {
throw new AuthenticationException("用户账号已禁用! account=" + jwtToken.getAuthAccount());
} else if (Cons.DICTCODE_ACCOUNT_STATUS.L.name().equals(latestAccount.getStatus())) {
throw new AuthenticationException("用户账号已锁定! account=" + jwtToken.getAuthAccount());
} else {
AccountMemoryCache.put(jwtToken.getAuthAccount(), latestAccount);
return latestAccount;
}
}
public String applyToken(AuthCredential credential) {
BaseJwtAuthToken authToken = this.initBaseJwtAuthToken(credential);
try {
Subject subject = SecurityUtils.getSubject();
subject.login(authToken);
if (subject.isAuthenticated()) {
log.debug("申请token成功!authtoken={}", authToken.getCredentials());
this.saveLoginTrace(authToken, true);
return (String) authToken.getCredentials();
} else {
log.error("认证失败");
this.saveLoginTrace(authToken, false);
throw new BusinessException(Status.FAIL_OPERATION, "认证失败");
}
} catch (Exception var4) {
log.error("登录异常", var4);
this.saveLoginTrace(authToken, false);
throw new BusinessException(Status.FAIL_OPERATION, var4.getMessage());
}
}
/**
* 初始化token
* @param {Object} AuthCredential credential
*/
private BaseJwtAuthToken initBaseJwtAuthToken(AuthCredential credential) {
BaseJwtAuthToken token = new BaseJwtAuthToken(this.getAuthType(), credential.getUserTypeClass());
token.setAuthAccount(credential.getAuthAccount());
token.setRememberMe(credential.isRememberMe());
return token.generateAuthtoken(this.getExpiresInMinutes());
}
/**
* 保存日志
* @param authToken
* @param isSuccess
*/
protected void saveLoginTrace(BaseJwtAuthToken authToken, boolean isSuccess) {
IamLoginTrace loginTrace = new IamLoginTrace();
loginTrace.setAuthType(this.getAuthType()).setAuthAccount(authToken.getAuthAccount()).setUserType(authToken.getUserType()).setSuccess(isSuccess);
BaseLoginUser currentUser = (BaseLoginUser) IamSecurityUtils.getCurrentUser();
if (currentUser != null) {
loginTrace.setUserId(currentUser.getId());
}
String userAgent = this.request.getHeader("user-agent");
String ipAddress = IamSecurityUtils.getRequestIp(this.request);
loginTrace.setUserAgent(userAgent).setIpAddress(ipAddress);
try {
this.iamLoginTraceService.createEntity(loginTrace);
} catch (Exception var8) {
log.warn("保存登录日志异常", var8);
}
}
}修改登陆接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* 用户登录获取token
* 基于无状态,增加了一个长期的refreshtoken,根据自己需要添加
*
* @param credential
* @return
* @throws Exception
*/
public JsonResult login( WXMemberCredential credential)throws Exception {
// 替换service 获取token
String authtoken = AuthServiceFactory.getAuthService(Cons.DICTCODE_AUTH_TYPE.WX_MP.name()).applyToken(credential);
return JsonResult.OK(new HashMap<String, String>(8){{
put("authtoken", authtoken);
// 刷新token,长期有效
put("refreshtoken", JwtUtils.generateToken(credential.getAuthAccount(), 60 * 24 * 30 * 12 * 10));
}});
}至此,我们将自定义扩展登陆、用户信息处理完毕!!!