环境
springboot 3.1.5
maven:3.6.1
java:17
思路
- 续期在后端完成,只给前端传递一个token,不采用两个token的方式,从而不增加前端工作量
- 默认情况下解析Jwt token 时过期的token将抛出过期错误,因为需要自动续期,因此应忽略该错误。见代码片段1
- token等信息保存在Redis中,并设置过期时间为token的过期时间。过期后保存信息自动删除 。修改此处过期时间实现延期
- 记录每次前端请求的时间至Redis中。
- 验证token有效性时,若距离最近一次请求的时差未超过定义的过期时间增量,则尝试进行续期操作。见代码片段2。代码注解很详细,就不在重复
代码片段1
/**
* 获取Claims
*
* @param bearerToken 带前缀的token
* @return Claims
*/
public static Claims getClaims(String bearerToken) {
if (StringUtils.isBlank(bearerToken)) {
return null;
}
Claims claims = null;
try {
String token = formBearerTokenToToken(bearerToken);
if (StringUtils.isNotBlank(token)) {
claims = Jwts.parser().setSigningKey(JwtParams.getSecret()).parseClaimsJws(token).getBody();
}
} catch (ExpiredJwtException e) {
//过期依然返回claims
claims = e.getClaims();
// log.info("获取Claims时已过期, Error:{}; token:{}", e.getLocalizedMessage(), bearerToken);
} catch (Exception e) {
log.info("获取Claims时出错,token:{}; Error:{}", bearerToken, e.getLocalizedMessage());
}
return claims;
}
代码片段2
/**
* @description 判断request的token是否有效,若过期尝试自动续期
* <br>
* <pre>
* 判断及操作流程:
* 1.从request中获取token,其是否格式正确(前缀及签发者)且含有userId。为假,直接返回false
* 2.判断request中token是否与cache中的token一致。不一致,直接返回false
* 3.request中的token,是否未过期
* 3.1 token未过期,直接返回true!!
* 3.2 token过期,判断最后一次操作与当前时间差是否超过配置的过期时间增量(JwtParams.getExpireTime())
* 3.2.1 超过配置的过期时间增量,返回false
* 3.2.2 未超过,尝试自动续期
* 3.2.2.1 自动续期成功,返回true!!
* 3.2.2.2 自动续期失败,返回false
* <pre>
* @param request request
* @return boolean 是否成功
* @author MuYi
* @date 2023/12/12 12:57
*/
public boolean tokenIsValid(HttpServletRequest request) {
if (request == null) return true;
String bearerToken = JwtTokenUtils.getToken(request);
// 验证request是否格式及签发者正确、是否含userId
if (!JwtTokenUtils.tokenIsCorrect(bearerToken)) return false;
Long userId = JwtTokenUtils.getUserId(bearerToken);
if (userId == null) return false;
String requestInfo = request.getLocalAddr() + ":" + request.getLocalPort() + request.getRequestURI();
// 验证request与cache中的token是否一致
String cacheToken = JwtCacheUtils.getTokenFromCache(userId);
//cache中无token。表示用户未登录或已注销或系统重启
//依据前提:
// 1 token的核验以cache中的token为准
// 2 当用户登录时cache中保存其token
// 3 当用户注销时、系统关闭时删除其cache中的token
if (cacheToken == null) {
forceLogout(userId, "使用已注销令牌", requestInfo);
return false;
}
if (!bearerToken.equals(cacheToken)) {
forceLogout(userId, "令牌不一致", requestInfo);
return false;
}
// 验证request中的token是否未过期
if (!JwtTokenUtils.tokenIsExpired(bearerToken)) return true;
//验证距离最后一次操作时间是否超过配置的过期时间增量
Long lastOperateTime = JwtCacheUtils.getLastOperateTime(userId);
//计算距离最后一次操作时间差,增加TOKEN_ADVANCE_CHECK_TIME时间量,以免出现误差
long diff = System.currentTimeMillis()+TOKEN_ADVANCE_CHECK_TIME - lastOperateTime;
if (diff > JwtParams.getExpireTime()) {
forceLogout(userId, "令牌过期", requestInfo);
return false;
}
//尝试自动续期
Boolean renewal = JwtCacheUtils.renewalTokenExpireTime(userId);
if (renewal) {
return true;
} else {
forceLogout(userId, "尝试自动续期失败", requestInfo);
return false;
}
}
protected void forceLogout(Long userId, String msg, String requestInfo) {
try {
log.info("{},将强制用户(ID={})退出。(request:{})", msg, userId, requestInfo);
logoutProcess(userId);
// request.setAttribute("renewal",false);
} catch (Exception e) {
log.info("{},将强制用户(ID={})退出。(request:{})。发生错误{}", msg, userId, requestInfo, e.getLocalizedMessage());
}
}