Spring Boot(Spring Security + JWT)应用的登出功能实现方案
老哥,我太懂你这个需求了——之前做Spring Boot+Spring Security+JWT项目的时候,也觉得只让客户端删token的登出方案太敷衍,毕竟只要token还在有效期内,万一被窃取就仍能正常访问接口,必须在后端把这个安全口子堵上!下面给你捋几个实战过的靠谱实现方案:
核心思路:让已签发的JWT失效
因为JWT本身是无状态的,一旦签发就没法主动撤销,所以后端实现登出的核心逻辑就是记录“无效的token列表”,在每次请求校验JWT时,先检查该token是否在无效列表里,是就拒绝请求。
方案1:基于Redis的令牌黑名单(最常用)
这是生产环境中最稳妥的方案,利用Redis的过期自动清理特性,完美匹配JWT的有效期:
- 登录签发JWT时:把token(或者token的唯一标识
jti)作为Redis的key,过期时间设置成和JWT的有效期一致,存入Redis(可以标记为“有效”,不过其实登出时才需要存入黑名单)。 - 登出接口逻辑:接收客户端传来的token,先校验token的有效性,然后将其存入Redis的黑名单中,过期时间和原JWT的剩余有效期一致。
- 自定义JWT校验过滤器:在Spring Security的过滤器链里,每次校验JWT前,先检查Redis的黑名单中是否存在该token,如果存在就直接抛出“token已失效”的异常,拒绝请求。
代码示例
登出接口
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private JwtTokenProvider jwtTokenProvider; @PostMapping("/logout") public ResponseEntity<String> logout(HttpServletRequest request) { // 从请求头中提取token(假设存在Authorization: Bearer <token>) String token = jwtTokenProvider.extractTokenFromRequest(request); // 先校验token是否有效,防止恶意提交无效token if (!jwtTokenProvider.validateToken(token)) { return ResponseEntity.badRequest().body("Invalid token"); } // 解析token的过期时间,计算剩余有效期 Claims claims = jwtTokenProvider.getClaimsFromToken(token); long remainingTime = claims.getExpiration().getTime() - System.currentTimeMillis(); // 存入黑名单,设置过期时间自动清理 redisTemplate.opsForValue() .set("jwt:blacklist:" + token, "invalid", remainingTime, TimeUnit.MILLISECONDS); return ResponseEntity.ok("Logout successful"); } }
JWT校验时的黑名单检查
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate redisTemplate; @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = jwtTokenProvider.extractTokenFromRequest(request); if (token != null && jwtTokenProvider.validateToken(token)) { // 检查是否在黑名单中 if (redisTemplate.hasKey("jwt:blacklist:" + token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token has been revoked"); return; } // 后续的认证逻辑... } filterChain.doFilter(request, response); } }
方案2:基于用户的令牌失效列表(单实例场景)
如果你的应用是单实例部署,不想引入Redis,也可以用内存维护每个用户的有效令牌列表:
- 登录时:把用户ID和对应的有效token存入
ConcurrentHashMap<String, List<String>>(比如key是用户ID,value是该用户的所有有效token)。 - 登出时:根据用户ID移除对应的token,甚至可以直接清空该用户的所有token(实现强制下线所有设备的功能)。
- 校验时:检查当前用户的有效token列表中是否包含该token,不包含则拒绝请求。
注意:这个方案只适合单实例应用,集群环境下必须用分布式缓存(比如Redis)同步失效列表,否则不同节点的状态不一致,会出现部分节点仍允许过期token访问的问题。
方案3:缩短JWT有效期+刷新令牌(增强安全性)
配合上面的黑名单方案一起用,能进一步降低风险:
- 把
access token的有效期设短(比如15分钟),同时签发refresh token(有效期设长一些,比如7天)。 - 客户端用
access token请求业务接口,过期后用refresh token去刷新接口获取新的access token。 - 登出时:不仅要把
access token加入黑名单,还要把refresh token也加入黑名单,防止用户用refresh token重新获取有效access token。
方案4:JWT撤销声明(不推荐)
有些JWT库支持在payload中加入自定义的revoked声明,或者利用nbf(Not Before)字段,但这种方式需要后端维护每个token的状态,每次校验都要查询数据库或缓存,既失去了JWT无状态的优势,性能也不如黑名单方案,所以一般不推荐在生产环境使用。
总结
优先推荐Redis黑名单+刷新令牌的组合方案,既保证了安全性,又兼顾了性能和可扩展性。实现时还要注意:
- 登出接口一定要先校验token的有效性,防止恶意提交无效token占用Redis空间。
- Redis的key要设置合理的过期时间,避免内存泄漏。
- 集群环境下必须保证Redis是共享的,所有节点都能访问到黑名单数据。
内容的提问来源于stack exchange,提问作者Hleb
相关产品推荐
相关产品推荐

