在上一篇文章介绍 youlai-mall 项目中,经过整合Spring Cloud Gateway、Spring Security OAuth二、JWT等技术实现了微服务下统一认证受权平台的搭建。最后在文末留下一个值得思考问题,就是如何在注销、修改密码、修改权限场景下让JWT失效?因此在这篇文章来对方案和实现进行补充。想亲身体验的小伙伴们能够了解下 youlai-mall 项目和Spring Cloud实战系列往期文章。html
Spring Cloud实战系列往期文章vue
JWT最大的一个优点在于它是无状态的,自身包含了认证鉴权所须要的全部信息,服务器端无需对其存储,从而给服务器减小了存储开销。java
可是无状态引出的问题也是可想而知的,它没法做废未过时的JWT。举例说明注销场景下,就传统的cookie/session认证机制,只须要把存在服务器端的session删掉就OK了。可是JWT呢,它是不存在服务器端的啊,好的那我删存在客户端的JWT行了吧。额,社会本就复杂别再欺骗本身了好么,被你在客户端删掉的JWT仍是能够经过服务器端认证的。git
首先明确一点JWT失效的惟一途径就是等过时,就是说不借助外力的状况下,没法达到某些场景下须要主动使JWT失效的目的。而外力则是在服务器端存储着JWT的状态,在请求资源时添加判断逻辑,这与JWT特性无状态是相互矛盾的存在。可是,你要知道若是你选择走上了JWT这条路,那就没得选了。若是你有好的方式,但愿你来打我脸。github
如下就JWT在某些场景须要失效的简单方案整理以下:web
1. 白名单方式redis
认证经过时,把JWT缓存到Redis,注销时,从缓存移除JWT。请求资源添加判断JWT在缓存中是否存在,不存在拒绝访问。这种方式和cookie/session机制中的会话失效删除session基本一致。json
2. 黑名单方式小程序
注销登陆时,缓存JWT至Redis,且缓存有效时间设置为JWT的有效期,请求资源时判断是否存在缓存的黑名单中,存在则拒绝访问。
白名单和黑名单的实现逻辑差很少,黑名单不需每次登陆都将JWT缓存,仅仅在某些特殊场景下须要缓存JWT,给服务器带来的压力要远远小于白名单的方式。
如下演示在退出登陆时经过添加至黑名单的方式实现JWT失效
逻辑很明确,在调用退出登陆接口时将JWT缓存到Redis的黑名单中,而后在网关作断定请求头的JWT是否在黑名单内作对应的处理。
登出接口/oauth/logout的主要逻辑把JWT添加至Redis黑名单缓存中,但不必把整个JWT字符串都存储下来,JWT的载体中有个jti(JWT ID)字段声明为JWT提供了惟一的标识符。JWT解析的结构以下:
既然有这么个字段能做为JWT的惟一标识,从JWT解析出jti以后将其存储到黑名单中做为判别依据,相较于存储完整的JWT字符串减小了存储开销。另外咱们只需保证JWT在其有效期内用户登出后失效就能够了,JWT有效期过了黑名单也就没有存在的必要,因此咱们这里还须要设置黑名单的过时时间,否则黑名单的数量会无休止的愈来愈多,这是咱们不想看到的。
@Api(tags = "认证中心") @RestController @RequestMapping("/oauth") @AllArgsConstructor public class AuthController { private RedisTemplate redisTemplate; @DeleteMapping("/logout") public Result logout(HttpServletRequest request) { String payload = request.getHeader(AuthConstants.JWT_PAYLOAD_KEY); JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); // JWT惟一标识 long exp = jsonObject.getLong("exp"); // JWT过时时间戳(单位:秒) long currentTimeSeconds = System.currentTimeMillis() / 1000; if (exp < currentTimeSeconds) { // token已过时 return Result.custom(ResultCode.INVALID_TOKEN_OR_EXPIRED); } redisTemplate.opsForValue().set(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (exp - currentTimeSeconds), TimeUnit.SECONDS); return Result.success(); } }
从请求头提取JWT,解析出惟一标识jti,而后判断该标识是否存在黑名单列表里,若是是直接返回响应token失效的提示信息。
/** * 全局过滤器 黑名单token过滤 */ @Component @Slf4j @AllArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered { private RedisTemplate redisTemplate; @SneakyThrows @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER); if (StrUtil.isBlank(token)) { return chain.filter(exchange); } token = token.replace(AuthConstants.JWT_TOKEN_PREFIX, Strings.EMPTY); JWSObject jwsObject = JWSObject.parse(token); String payload = jwsObject.getPayload().toString(); // 黑名单token(登出、修改密码)校验 JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); // JWT惟一标识 Boolean isBlack = redisTemplate.hasKey(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti); if (isBlack) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin", "*"); response.getHeaders().set("Cache-Control", "no-cache"); String body = JSONUtil.toJsonStr(Result.custom(ResultCode.INVALID_TOKEN_OR_EXPIRED)); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } ServerHttpRequest request = exchange.getRequest().mutate() .header(AuthConstants.JWT_PAYLOAD_KEY, payload) .build(); exchange = exchange.mutate().request(request).build(); return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
测试流程涉及到如下3个接口
1. 登陆访问资源
2. 退出登陆再次访问资源
退出成功查看redis缓存黑名单列表
再次访问登陆用户信息以下:
能够看到退出登陆后再次使用原JWT请求提示“token无效或已过时”
3. youlai-mall项目退出登陆演示
上面报“token无效或已过时”的响应码是"A0230",这个对应的是Java开发手册【泰山版】的错误码
打开以前搭建好的前端管理平台youlai-mall-admin-web,修改src/util/request.js文件中的无效token的响应码为“A0230”,这样在token无效的状况下提示从新登陆
演示经过第三方接口调试工具调用注销接口让JWT失效,而后再次刷新页面请求资源会由于JWT的失效而跳转到登陆页。
JWT是JSON风格轻量级的受权和身份认证规范,可实现无状态、分布式应用的统一认证鉴权。可是事物每每具备两面性,有利必有弊,由于JWT的无状态,自生成后不借助外界条件惟一失效的方式就是过时。然而借助的外界的条件后JWT便有状态了的,也就是没有所谓严格意义上的无状态,其实也没必要纠结于此,由于瑕不掩瑜。在白名单和黑名单的实现方式,这里选择了后者状态性更小的黑名单方式。仍是文中提到过的一句话,若是你有更好的实现方式,欢迎留言告知,不胜感激!
本篇是暂阶段的Spring Cloud实战的最终章了,也就是说基于Spring Boot +Spring Cloud+ Element-UI搭建的先后端分离基础权限框架已经搭建完成。后面计划写使用此基础框架整合uni-app跨平台前端框架开发一套商城小程序,但愿你们给个关注或star,感谢感谢~
本篇完整代码下载地址: