You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:28:37