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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 05:25:10