使用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; }