
jwt 本身无状态且不可撤销,登出时无法真正“销毁”令牌;正确做法是结合黑名单(token blacklist)或短期有效期 + 刷新机制,在服务端主动拒绝已登出令牌的后续请求。
jwt 本身无状态且不可撤销,登出时无法真正“销毁”令牌;正确做法是结合黑名单(token blacklist)或短期有效期 + 刷新机制,在服务端主动拒绝已登出令牌的后续请求。
在 Spring Boot + Spring Security 项目中,使用 JWT 实现登出(Logout)常被误解为“删除客户端持有的 Token”,但这是无效的——因为 JWT 是自包含、签名验证的无状态凭证,一旦签发,只要未过期且签名有效,服务端就默认信任它。因此,真正的登出必须由服务端主动干预验证逻辑。
✅ 推荐方案:JWT 黑名单(Blacklist)机制
核心思路:用户登出时,将当前 JWT 的 jti(唯一令牌标识)或 token hash 存入 Redis / 数据库黑名单,并在每次请求的 JWT 过滤器中额外校验该令牌是否已被标记为“已登出”。
1. 修改 JWT 工具类,支持生成/解析 jti
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.setId(UUID.randomUUID().toString()) // ✅ 关键:设置唯一 jti
.signWith(getSignInKey(), SignatureAlgorithm.HS512)
.compact();
}
public String getJtiFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody()
.getId(); // 获取 jti
}2. 登出接口:将 jti 写入 Redis(推荐)或数据库
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestBody LogoutRequest request) {
try {
String jti = jwtUtils.getJtiFromToken(request.getToken());
// 存入 Redis,过期时间 = JWT 剩余有效期(可从 token 解析)
Long expiration = jwtUtils.getExpirationFromToken(request.getToken()).getTime();
long remainingSeconds = (expiration - System.currentTimeMillis()) / 1000;
redisTemplate.opsForValue()
.set("blacklist:" + jti, "invalid", Duration.ofSeconds(remainingSeconds));
return ResponseEntity.ok("Logged out successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid token");
}
}3. 自定义 JWT 过滤器:增加黑名单校验
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateToken(jwt)) {
// ✅ 新增:检查是否在黑名单中
String jti = jwtUtils.getJtiFromToken(jwt);
if (redisTemplate.hasKey("blacklist:" + jti)) {
logger.warn("Blocked blacklisted token: {}", jti);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token has been revoked");
return;
}
// 继续认证流程...
UsernamePasswordAuthenticationToken authentication = ...;
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}⚠️ 注意事项与最佳实践
-
不要依赖“删除 Refresh Token”代替 JWT 登出:你的代码中
deleteByUserId仅清理刷新令牌,但原 Access Token 仍有效,攻击者若截获该 Token 仍可继续访问。 - 避免数据库高频查询:黑名单务必使用 Redis 等内存存储,保证 O(1) 查询性能;禁止在每次请求中查 MySQL。
- 合理设置 Token 有效期:Access Token 建议 ≤ 15 分钟,配合 Refresh Token(带存储、可撤销)实现长期会话,降低登出窗口风险。
-
前端必须清除本地 Token:虽服务端已拦截,但仍需前端
localStorage.removeItem("jwtToken")防止误用。 -
替代方案考虑 Session + Cookie:如无需分布式无状态特性,Spring Security 原生
FormLogin+HttpOnly Secure Cookie更安全(自动支持登出、CSRF 防护、SameSite 等)。
? 总结:JWT 登出的本质不是“删 Token”,而是“让 Token 失效”。通过
jti + Redis 黑名单 + 过滤器增强校验,即可在保持 JWT 优势的同时,实现可控、高效、安全的登出体验。切勿自行构建类 Session 的复杂状态管理,优先复用 Spring Security 成熟机制。


















