使用shiro jwt做应用系统的权限校验,网上通用的方式是这样的。
在用户登录时候会生成两份token,一份AccessToken用于返回给前端,前端带上这个令牌去请求后台接口,通常过期时间较短5分钟左右,一份RefreshToken放在Redis中,两个Token的加密值都是当前时间戳,当用户的AccessToken过期时,就去从Redis中通过用户名取得Redis中的RefreshToken,比较AccessToken和RefreshToken中的时间戳是否一致,如果一致就重新生成一组AccessToken和RefreshToken,他们值都是当前时间戳。然后返回给前端。用户继续带着这个新AccessToken请求接口,原则上如果用户持续点击,就可以一致无限的刷新Token,但是这种方式如果页面都是单请求,自然没有问题,但是页面不可避免的都会有并发的请求。如果有并发请求过来,只有第一个请求可以正常返回,其他的后续所有请求都会失败,因为第一个请求已经刷新的Redis中的时间戳,返回给了前端的新的AccessToken,后续的并发请求自然带着老AccessToken就会全部报错。
这种情况怎么解决呢?
1. 保证前端请求的顺序执行,这种方式比较消耗前端性能接口请求较慢
2. 在后端刷新RefreshToken写入Redis时加入Redis写入锁,只有最先最快拿到锁的线程允许修改Redis中的值,锁定时间设置为30s,30s后解锁,写入数据。
3.缓存旧的AccessToken,每次需要刷新Token的时候都把老Token存储一份,可以放在Redis中,或者一个全局变量中,有效时间30s。也就是说30s内带着老Token请求依然有效
这里主要详细描述下第三种方式如何实现。
//全局变量用于缓存旧的Token
Map<String,Long> tempToken = new HashMap<>();
/**
* shiro验证成功调用
* @param token
* @param subject
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
String jwttoken= (String) token.getPrincipal();
if (jwttoken!=null){
try{
if(TokenUtil.verify(jwttoken)){
//判断Redis是否存在所对应的RefreshToken
String account = TokenUtil.getAccount(jwttoken);
Long currentTime=TokenUtil.getCurrentTime(jwttoken);
if (RedisUtil.hasKey(account)) {
Long currentTimeMillisRedis = (Long) RedisUtil.get(account);
if (currentTimeMillisRedis.equals(currentTime)) {
return true;
}
}
}
return false;
}catch (Exception e){
if (e instanceof TokenExpiredException){
System.out.println("AccessToken过期了");
if(tempToken.containsKey(jwttoken)){
// 判断缓存中是否有token,有的话判断时间是否小于30s,小于30 通过,大于30 清空map
Long currentTimeMillis =System.currentTimeMillis();
long r = (currentTimeMillis - tempToken.get(jwttoken))/1000;
if(r <= 30 ){
System.out.println("小于30 所以通过");
return true;
}else{
System.out.println("大于30 所以通过");
tempToken.clear();
return false;
}
}
if (refreshToken(request, response)) {
return true;
}else {
return false;
}
}
}
}
return true;
}
/**
* 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
* @param request
* @param response
* @return
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
String token = ((HttpServletRequest)request).getHeader("X-Auth-Token");
String account = TokenUtil.getAccount(token);
Long currentTime=TokenUtil.getCurrentTime(token);
// 判断Redis中RefreshToken是否存在
if (RedisUtil.hasKey(account)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
Long currentTimeMillisRedis = (Long) RedisUtil.get(account);
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (currentTimeMillisRedis.equals(currentTime)) {
// 获取当前最新时间戳
Long currentTimeMillis =System.currentTimeMillis();
tempToken.put(token,currentTimeMillis);
RedisUtil.set(account, currentTimeMillis,
CommonData.REFRESH_EXPIRE_TIME);
// 刷新AccessToken,设置时间戳为当前最新时间戳
token = TokenUtil.sign(account, currentTimeMillis);
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
}
}
return false;
}