Spring MVC中JWT令牌失效实现登出功能的技术咨询
嘿,刚好碰到过类似的问题!JWT本身是无状态、自包含的特性,导致没法直接让已签发的令牌“作废”,得借助额外机制来实现登出功能。结合你用Spring MVC搭后端、Angular 5做前端的栈,给你几个落地性强的方案:
方案1:令牌黑名单机制(生产环境最常用)
核心思路是维护一个已失效令牌的列表,在每次验证JWT前先检查令牌是否在黑名单中。步骤如下:
- 1. 选择黑名单存储:推荐用Redis(自带过期时间,自动清理失效条目);如果是单实例测试环境,也可以用
ConcurrentHashMap临时替代(生产环境禁止用内存存储,多实例会导致黑名单不一致) - 2. 实现登出接口:前端调用登出接口时,将当前令牌存入黑名单,并设置过期时间与令牌自身的
exp(过期时间)一致 - 3. 改造JWT验证逻辑:在拦截器/过滤器中,先检查令牌是否在黑名单,再执行常规的JWT签名验证
后端代码片段(Spring MVC)
登出接口
@RestController @RequestMapping("/api/auth") public class AuthController { private static final Logger logger = LoggerFactory.getLogger(AuthController.class); @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private JwtUtil jwtUtil; // 你已编写的JWT工具类 @PostMapping("/logout") public ResponseEntity<String> logout(@RequestHeader("Authorization") String authHeader) { // 提取Bearer令牌 String token = authHeader.replace("Bearer ", "").trim(); // 获取令牌的过期时间 Date expireDate = jwtUtil.extractExpiration(token); long expireMillis = expireDate.getTime() - System.currentTimeMillis(); // 将令牌存入黑名单,设置过期时间与令牌一致 redisTemplate.opsForValue() .set("JWT_BLACKLIST:" + token, "invalid", expireMillis, TimeUnit.MILLISECONDS); logger.info("Token {} added to blacklist", token); return ResponseEntity.ok("Logout successful"); } }
JWT验证过滤器改造
public class JwtAuthFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); if (token != null) { // 先检查黑名单 if (redisTemplate.hasKey("JWT_BLACKLIST:" + token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token has been invalidated"); return; } // 执行原有JWT验证逻辑 if (jwtUtil.validateToken(token)) { // 将用户信息存入SecurityContext(如果用Spring Security的话) // ...你的原有验证逻辑 } } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); return authHeader != null && authHeader.startsWith("Bearer ") ? authHeader.substring(7) : null; } }
前端代码片段(Angular 5)
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable() export class AuthService { private apiUrl = '/api/auth'; constructor(private http: HttpClient) {} logout(): Promise<void> { const token = localStorage.getItem('jwtToken'); return this.http.post(`${this.apiUrl}/logout`, {}, { headers: { 'Authorization': `Bearer ${token}` } }).toPromise().then(() => { // 清除本地存储的令牌 localStorage.removeItem('jwtToken'); // 跳转到登录页 window.location.href = '/login'; }).catch(err => { console.error('Logout failed:', err); // 即使后端出错,也清除本地令牌 localStorage.removeItem('jwtToken'); throw err; }); } }
方案2:短有效期访问令牌+刷新令牌
如果不想维护大量黑名单条目,可以把访问令牌(Access Token)的有效期设得很短(比如15分钟),同时签发一个刷新令牌(Refresh Token)(有效期几小时/几天)。登出时:
- 前端清除本地的访问令牌和刷新令牌
- 后端将刷新令牌加入黑名单(因为刷新令牌有效期长,黑名单条目少)
这个方案的优势是黑名单的存储压力小,适合用户登录状态持续时间较长的场景。代码逻辑和方案1类似,只是登出时处理的是刷新令牌。
方案3:前端主动清除令牌(仅前端层面)
这是最简单但安全性最低的方案:前端直接清除本地存储的JWT令牌,不再携带令牌请求接口。但后端的令牌仍然有效,恶意用户如果拿到令牌仍可继续访问。适合低安全要求的测试场景:
logout(): void { localStorage.removeItem('jwtToken'); sessionStorage.removeItem('jwtToken'); window.location.href = '/login'; }
总结
生产环境优先选方案1(令牌黑名单)或方案2(刷新令牌),根据你的架构和安全需求决定。如果是分布式系统,一定要用Redis这类分布式存储来维护黑名单,避免多实例数据不一致的问题。
内容的提问来源于stack exchange,提问作者faserx
相关产品推荐
相关产品推荐

