综上考虑,还是采用黑名单的方式来实现注销登录功能,实时统计在线人数和踢出用户等功能作为扩展功能来开发,不在登录注销逻辑中掺杂太多的业务处理逻辑,使系统保持低耦合。
为了使JWT有效信息最大程度保证准确性,注销登录除了在系统点击退出登录按钮,还需要监测是否直接关闭页面,关闭浏览器事件,来执行调用系统注销接口。
token和refresh_token的过期时间不一致,都在其解析之后的exp字段。因为我们定制了黑名单模式,当用户点击退出登录之后,我们会把refresh_token也加入黑名单,在refresh_token获取刷新token的时候,需要定制校验refresh_token是否被加入到黑名单。
1、退出登录接口将token和refresh_token加入黑名单
/** * 退出登录需要需要登录的一点思考: * 1、如果不需要登录,那么在调用接口的时候就需要把token传过来,且系统不校验token有效性,此时如果系统被攻击,不停的大量发送token,最后会把redis充爆 * 2、如果调用退出接口必须登录,那么系统会调用token校验有效性,refresh_token通过参数传过来加入黑名单 * 综上:选择调用退出接口需要登录的方式 * @param request * @return */ @PostMapping("/logout") public Result logout(HttpServletRequest request) { String token = request.getHeader(AuthConstant.JWT_TOKEN_HEADER); String refreshToken = request.getParameter(AuthConstant.REFRESH_TOKEN); long currentTimeSeconds = System.currentTimeMillis() / GitEggConstant.Number.THOUSAND; // 将token和refresh_token同时加入黑名单 String[] tokenArray = new String[GitEggConstant.Number.TWO]; tokenArray[GitEggConstant.Number.ZERO] = token.replace("Bearer ", ""); tokenArray[GitEggConstant.Number.ONE] = refreshToken; for (int i = GitEggConstant.Number.ZERO; i < tokenArray.length; i++) { String realToken = tokenArray[i]; JSONObject jsonObject = JwtUtils.decodeJwt(realToken); String jti = jsonObject.getAsString("jti"); Long exp = Long.parseLong(jsonObject.getAsString("exp")); if (exp - currentTimeSeconds > GitEggConstant.Number.ZERO) { redisTemplate.opsForValue().set(AuthConstant.TOKEN_BLACKLIST + jti, jti, (exp - currentTimeSeconds), TimeUnit.SECONDS); } } return Result.success(); }2、Gateway在AuthorizationManager中添加token是否加入黑名单的判断
//如果token被加入到黑名单,就是执行了退出登录操作,那么拒绝访问 String realToken = token.replace("Bearer ", ""); try { JWSObject jwsObject = JWSObject.parse(realToken); Payload payload = jwsObject.getPayload(); JSONObject jsonObject = payload.toJSONObject(); String jti = jsonObject.getAsString("jti"); String blackListToken = (String)redisTemplate.opsForValue().get(AuthConstant.TOKEN_BLACKLIST + jti); if (!StringUtils.isEmpty(blackListToken)) { return Mono.just(new AuthorizationDecision(false)); } } catch (ParseException e) { e.printStackTrace(); }3、自定义DefaultTokenService,校验refresh_token是否被加入黑名单
@Slf4j public class GitEggTokenServices extends DefaultTokenServices { private final RedisTemplate redisTemplate; public GitEggTokenServices(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Transactional( noRollbackFor = {InvalidTokenException.class, InvalidGrantException.class} ) @Override public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException { JSONObject jsonObject = null; String jti = null; //如果refreshToken被加入到黑名单,就是执行了退出登录操作,那么拒绝访问 try { JWSObject jwsObject = JWSObject.parse(refreshTokenValue); Payload payload = jwsObject.getPayload(); jsonObject = payload.toJSONObject(); jti = jsonObject.getAsString(TokenConstant.JTI); String blackListToken = (String)redisTemplate.opsForValue().get(AuthConstant.TOKEN_BLACKLIST + jti); if (!StringUtils.isEmpty(blackListToken)) { throw new InvalidTokenException("Invalid refresh token (blackList): " + refreshTokenValue); } } catch (ParseException e) { log.error("获取refreshToken黑名单时发生错误:{}", e); } OAuth2AccessToken oAuth2AccessToken = super.refreshAccessToken(refreshTokenValue, tokenRequest); // RefreshToken不支持重复使用,如果使用一次,则加入黑名单不再允许使用,当刷新token执行完之后,即校验过RefreshToken之后,才执行存redis操作 if (null != jsonObject && !StringUtils.isEmpty(jti)) { long currentTimeSeconds = System.currentTimeMillis() / GitEggConstant.Number.THOUSAND; Long exp = Long.parseLong(jsonObject.getAsString(TokenConstant.EXP)); if (exp - currentTimeSeconds > GitEggConstant.Number.ZERO) { redisTemplate.opsForValue().set(AuthConstant.TOKEN_BLACKLIST + jti, jti, (exp - currentTimeSeconds), TimeUnit.SECONDS); } } return oAuth2AccessToken; } } 测试: