Spring Boot整合Spring Security:登出后如何失效JWT Token?
Spring Boot中JWT令牌失效的实现方案
核心思路
JWT本身是无状态令牌,签发后除非自然过期否则始终有效,因此要实现登出时失效令牌,需要结合黑名单机制处理访问令牌(Access Token) + 清理刷新令牌(Refresh Token) 两个环节:
- 用Redis存储已登出的Access Token黑名单,自动匹配令牌有效期清理条目
- 登出时同时失效当前Access Token和关联的Refresh Token
- 请求拦截时先校验令牌是否在黑名单中
具体实现步骤
1. 添加Redis依赖(若未引入)
在pom.xml中加入Redis starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2. 实现令牌黑名单服务
创建TokenBlacklistService处理黑名单的增查逻辑:
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class TokenBlacklistService { private final StringRedisTemplate redisTemplate; public TokenBlacklistService(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } // 将令牌加入黑名单,过期时间设为令牌剩余有效期 public void addToBlacklist(String token, long remainingTimeSeconds) { redisTemplate.opsForValue().set(token, "blacklisted", remainingTimeSeconds, TimeUnit.SECONDS); } // 检查令牌是否在黑名单中 public boolean isBlacklisted(String token) { return Boolean.TRUE.equals(redisTemplate.hasKey(token)); } }
3. 完善登出逻辑(修改AuthService)
结合你现有代码,更新logout方法,同时处理Access Token和Refresh Token:
@Service public class AuthService { private final TokenBlacklistService tokenBlacklistService; private final RefreshTokenRepository refreshTokenRepository; private final JwtTokenUtil jwtTokenUtil; // 假设你已有JWT工具类 private final UserService userService; // 构造函数注入所有依赖 public AuthService(TokenBlacklistService tokenBlacklistService, RefreshTokenRepository refreshTokenRepository, JwtTokenUtil jwtTokenUtil, UserService userService) { this.tokenBlacklistService = tokenBlacklistService; this.refreshTokenRepository = refreshTokenRepository; this.jwtTokenUtil = jwtTokenUtil; this.userService = userService; } public String logout(LogoutRequest request) { String token = request.getToken(); try { // 先尝试作为Access Token处理 if (jwtTokenUtil.validateToken(token)) { // 计算令牌剩余有效期,加入黑名单 long remainingSeconds = (jwtTokenUtil.getExpirationDateFromToken(token).getTime() - System.currentTimeMillis()) / 1000; if (remainingSeconds > 0) { tokenBlacklistService.addToBlacklist(token, remainingSeconds); } // 删除该用户所有Refresh Token Long userId = jwtTokenUtil.getIdFromToken(token); refreshTokenRepository.deleteByUserId(userId); return "登出成功,令牌已失效"; } } catch (Exception e) { // 解析失败,尝试作为Refresh Token处理 RefreshToken refreshToken = refreshTokenRepository.findByToken(token) .orElseThrow(() -> new RuntimeException("无效的刷新令牌")); // 删除当前Refresh Token refreshTokenRepository.delete(refreshToken); // 若关联的Access Token未过期,也加入黑名单(需RefreshToken实体存储accessToken字段) String accessToken = refreshToken.getAccessToken(); if (accessToken != null) { try { if (jwtTokenUtil.validateToken(accessToken)) { long remainingSeconds = (jwtTokenUtil.getExpirationDateFromToken(accessToken).getTime() - System.currentTimeMillis()) / 1000; if (remainingSeconds > 0) { tokenBlacklistService.addToBlacklist(accessToken, remainingSeconds); } } } catch (Exception ignored) { // Access Token已过期,无需处理 } } return "登出成功,刷新令牌已失效"; } return "无效的令牌"; } }
4. 添加JWT拦截器校验黑名单
修改你的JWT认证过滤器,在验证令牌前先检查是否在黑名单:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserDetailsService userDetailsService; private final TokenBlacklistService tokenBlacklistService; public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService, TokenBlacklistService tokenBlacklistService) { this.jwtTokenUtil = jwtTokenUtil; this.userDetailsService = userDetailsService; this.tokenBlacklistService = tokenBlacklistService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String jwtToken = null; if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); // 先检查令牌是否在黑名单 if (tokenBlacklistService.isBlacklisted(jwtToken)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "令牌已失效"); return; } } // 原有JWT验证逻辑 String username = null; if (jwtToken != null) { try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (Exception e) { logger.error("JWT令牌解析失败", e); } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } }
可选优化方案
如果业务允许,可以缩短Access Token的有效期(比如15分钟),登出时只需要清理Refresh Token即可。用户无法通过Refresh Token获取新的Access Token,现有Access Token会很快自然过期,这种方式更轻量,无需维护黑名单。
内容的提问来源于stack exchange,提问作者Sercan Noyan Germiyanoğlu
相关产品推荐
相关产品推荐

