Spring Cloud实战 | 最终篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案
一. 前言
在上一篇文章介绍 youlai-mall 项目中,通过整合Spring Cloud Gateway、Spring Security OAuth2、JWT等技术实现了微服务下统一认证授权平台的搭建。最后在文末留下一个值得思考问题,就是如何在注销、修改密码、修改权限场景下让JWT失效?所以在这篇文章来对方案和实现进行补充。想亲身体验的小伙伴们可以了解下 youlai-mall 项目和Spring Cloud实战系列往期文章。
Spring Cloud实战系列往期文章
- Spring Cloud实战 | 第一篇:Windows搭建Nacos服务
- Spring Cloud实战 | 第二篇:Spring Cloud整合Nacos实现注册中心
- Spring Cloud实战 | 第三篇:Spring Cloud整合Nacos实现配置中心
- Spring Cloud实战 | 第四篇:Spring Cloud整合Gateway实现API网关
- Spring Cloud实战 | 第五篇:Spring Cloud整合OpenFeign实现微服务之间的调用
- Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权
- vue-element-admin实战 | 第一篇: 移除mock接入后台,搭建有来商城youlai-mall前后端分离管理平台
- vue-element-admin实战 | 第二篇: 最小改动接入后台实现根据权限动态加载菜单
二. 解决方案
JWT最大的一个优势在于它是无状态的,自身包含了认证鉴权所需要的所有信息,服务器端无需对其存储,从而给服务器减少了存储开销。
但是无状态引出的问题也是可想而知的,它无法作废未过期的JWT。举例说明注销场景下,就传统的cookie/session认证机制,只需要把存在服务器端的session删掉就OK了。但是JWT呢,它是不存在服务器端的啊,好的那我删存在客户端的JWT行了吧。额,社会本就复杂别再欺骗自己了好么,被你在客户端删掉的JWT还是可以通过服务器端认证的。

首先明确一点JWT失效的唯一途径就是等过期,就是说不借助外力的情况下,无法达到某些场景下需要主动使JWT失效的目的。而外力则是在服务器端存储着JWT的状态,在请求资源时添加判断逻辑,这与JWT特性无状态是相互矛盾的存在。但是,你要知道如果你选择走上了JWT这条路,那就没得选了。如果你有好的方式,希望你来打我脸。
以下就JWT在某些场景需要失效的简单方案整理如下:
1. 白名单方式
认证通过时,把JWT缓存到Redis,注销时,从缓存移除JWT。请求资源添加判断JWT在缓存中是否存在,不存在拒绝访问。这种方式和cookie/session机制中的会话失效删除session基本一致。
2. 黑名单方式
注销登录时,缓存JWT至Redis,且缓存有效时间设置为JWT的有效期,请求资源时判断是否存在缓存的黑名单中,存在则拒绝访问。
白名单和黑名单的实现逻辑差不多,黑名单不需每次登录都将JWT缓存,仅仅在某些特殊场景下需要缓存JWT,给服务器带来的压力要远远小于白名单的方式。
三. 黑名单方式实现
以下演示在退出登录时通过添加至黑名单的方式实现JWT失效
逻辑很明确,在调用退出登录接口时将JWT缓存到Redis的黑名单中,然后在网关做判定请求头的JWT是否在黑名单内做对应的处理。
1. 认证中心(youlai-auth)退出登录接口
登出接口/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, Strings.EMPTY, (exp - currentTimeSeconds), TimeUnit.SECONDS);
return Result.success();
}
}
2. 网关(youlai-gateway)的全局过滤器
从请求头提取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. 注销后JWT失效测试
测试流程涉及到以下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,感谢感谢~
本篇完整代码下载地址:
Spring Cloud实战 | 最终篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案的更多相关文章
- Spring Cloud实战 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前后端分离模式下无感刷新实现JWT续期
一. 前言 记得上一篇Spring Cloud的文章关于如何使JWT失效进行了理论结合代码实践的说明,想当然的以为那篇会是基于Spring Cloud统一认证架构系列的最终篇.但关于JWT另外还有一个 ...
- Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理
本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...
- Spring cloud微服务实战——基于OAUTH2.0统一认证授权的微服务基础架构
https://blog.csdn.net/w1054993544/article/details/78932614
- [Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权
一. 前言 本篇实战案例基于 youlai-mall 项目.项目使用的是当前主流和最新版本的技术和解决方案,自己不会太多华丽的言辞去描述,只希望能勾起大家对编程的一点喜欢.所以有兴趣的朋友可以进入 g ...
- 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权
一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...
- Spring Cloud实战 | 第十一篇:Spring Cloud Gateway 网关实现对RESTful接口权限控制和按钮权限控制
一. 前言 hi,大家好,这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT实现的统 ...
- vue+uni-app商城实战 | 第一篇:【有来小店】微信小程序快速开发接入Spring Cloud OAuth2认证中心完成授权登录
一. 前言 本篇通过实战来讲述如何使用uni-app快速进行商城微信小程序的开发以及小程序如何接入后台Spring Cloud微服务. 有来商城 youlai-mall 项目是一套全栈商城系统,技术栈 ...
- Spring Cloud实战 | 第十篇 :Spring Cloud + Seata 1.4.1 + Nacos1.4.0 整合实现微服务架构中逃不掉的话题分布式事务
Seata分布式事务在线体验地址:https://www.youlai.store 本篇完整源码地址:https://github.com/hxrui/youlai-mall 有想加入开源项目开发的童 ...
- Spring Cloud实战: 基于Spring Cloud Gateway + vue-element-admin 实现的RBAC权限管理系统,实现网关对RESTful接口方法权限和自定义Vue指令对按钮权限的细粒度控制
一. 前言 信我的哈,明天过年. 这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT ...
随机推荐
- pandas 数据类型转换及描述统计
处理数据的时候往往需要对原始数据进行类型转换和预览等操作,下面介绍常用的处理预览和数据转换方法 预览:例: import pandas as pdsec_weather = pd.read_table ...
- python之常用正则表达式
以下整理python中常用的正则符号,相信能够熟悉掌握这些正则符号,大部分字符串处理将会游刃有余. 符号 含义 示例 . 可以匹配任意字符,但不包含换行符'\n' Pyt.on ->Pytmon ...
- 微信小程序发送订阅消息(之前是模板消息)
之前的模板消息已经废弃,现在改为订阅消息,订阅消息发布前,需要用户确认后才能接收订阅消息. 小程序端 index.wxml <button bindtap="send"> ...
- IDEA报错:Class JavaLaunchHelper is implemented in both
在IDEA运行一个程序时报错: Class JavaLaunchHelper is implemented in both 这个错误是Mac下Java 的一个bug,意思是这个JavaLaunchHe ...
- Java二进制和位运算,这一万字准能喂饱你
基础不牢,地动山摇.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习.关注公众号[BAT的乌托 ...
- 也谈基于Web的含工作流项目的一般开发流程
项目包含的通用模块代码等我有时间一并剥离贡献出来(基于WebSocket的通知引擎,工作流整合模块,自定义表单,基于RBAC权限设计),最近太忙了,Web项目有一段时间没碰,有点生疏的感觉,主要在忙G ...
- pyinstaller库的简单使用 打包科赫雪花几何图形
pyinstaller 简单使用 (cmd命令行) pyinstaller -F <文件名.py> Pyinstaller库常用参数 参数 描述 -h 查看帮助 --clean 清理打包过 ...
- private protected internal public
//C#中的访问修饰符: //private,私有访问修饰符,被private访问修饰符修饰的成员只有在当前类的内部可以访问,其他地方一律不能访问[类中成员,如果不写访问修饰符则默认都是私有的] // ...
- Unity图文混排
http://blog.csdn.net/akof1314/article/details/49028279 http://blog.csdn.net/akof1314/article/details ...
- 在CG/HLSL中访问着色器的内容
着色器在Properties代码块中声明 材质球的各种特性.如果你想要在着色器程序中使用这些特性,你需要在CG/HLSL中声明一个变量,这个变量需要与你要使用的特性拥有同样的名字和对的上号的类型.比如 ...