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

如何在Spring Boot中使用JWT实现刷新令牌解决Token过期问题

嘿,我来帮你搞定刷新令牌的实现!这玩意儿核心逻辑其实很简单:既然你的访问令牌(JWT)只有5分钟有效期,那我们就搭配一个有效期更长的刷新令牌,当访问令牌过期时,用这个刷新令牌去请求一个专门的接口,换取新的访问令牌,这样用户就不用反复登录了,体验会好很多。

下面我一步步给你讲怎么实现,结合你现有的Spring Security + JWT代码来调整:

一、核心逻辑梳理
  • 访问令牌(Access Token):就是你现在用的5分钟有效期的JWT,用来调用需要授权的API接口。
  • 刷新令牌(Refresh Token):有效期更长(比如7天),存在数据库/Redis中,和用户绑定,唯一且不可重复使用。
  • 流程:用户登录→获取访问令牌+刷新令牌→访问令牌过期→用刷新令牌调用刷新接口→获取新的访问令牌(可选同时换新的刷新令牌)→继续使用API。
二、代码实现步骤

1. 创建刷新令牌实体类和Repository

首先我们需要把刷新令牌持久化,这里用JPA示例(你也可以换成Redis,更适合分布式场景):

// RefreshToken实体类
@Entity
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user; // 关联你的用户实体

    @Column(nullable = false, unique = true)
    private String token;

    @Column(nullable = false)
    private Instant expiryDate;

    // 生成getter、setter
}
// RefreshToken的Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByToken(String token);
    Optional<RefreshToken> findByUser(User user);
    void deleteByToken(String token);
    void deleteAllByExpiryDateBefore(Instant instant); // 用于清理过期令牌
}

2. 扩展JWT工具类,支持生成刷新令牌

修改你现有的JWT工具类,添加生成刷新令牌的方法:

@Service
public class JwtService {
    @Value("${jwt.secret}")
    private String secretKey;
    @Value("${jwt.access-expiration:300000}") // 5分钟,单位毫秒
    private long accessExpiration;
    @Value("${jwt.refresh-expiration:604800000}") // 7天,单位毫秒
    private long refreshExpiration;

    // 生成访问令牌
    public String generateAccessToken(UserDetails userDetails) {
        return buildToken(userDetails, accessExpiration);
    }

    // 生成刷新令牌
    public String generateRefreshToken(UserDetails userDetails) {
        return buildToken(userDetails, refreshExpiration);
    }

    // 通用构建令牌的方法
    private String buildToken(UserDetails userDetails, long expiration) {
        Map<String, Object> claims = new HashMap<>();
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // 保留你现有的validateToken、extractUsername等方法
}

3. 修改登录接口,返回访问令牌+刷新令牌

登录成功后,不仅返回访问令牌,还要生成并保存刷新令牌:

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final JwtService jwtService;
    private final RefreshTokenRepository refreshTokenRepository;

    // 构造函数注入依赖

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
        // 验证用户名密码
        Authentication auth = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );
        SecurityContextHolder.getContext().setAuthentication(auth);
        
        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        String accessToken = jwtService.generateAccessToken(userDetails);
        String refreshToken = jwtService.generateRefreshToken(userDetails);

        // 保存/更新用户的刷新令牌
        RefreshToken savedToken = refreshTokenRepository.findByUser((User) userDetails)
                .map(existingToken -> {
                    existingToken.setToken(refreshToken);
                    existingToken.setExpiryDate(Instant.now().plusMillis(jwtService.getRefreshExpiration()));
                    return refreshTokenRepository.save(existingToken);
                })
                .orElseGet(() -> {
                    RefreshToken newToken = new RefreshToken();
                    newToken.setUser((User) userDetails);
                    newToken.setToken(refreshToken);
                    newToken.setExpiryDate(Instant.now().plusMillis(jwtService.getRefreshExpiration()));
                    return refreshTokenRepository.save(newToken);
                });

        // 返回令牌(建议把刷新令牌存到HttpOnly Cookie里,更安全)
        return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
    }

    // 响应和请求的DTO
    public record AuthResponse(String accessToken, String refreshToken) {}
    public record LoginRequest(String username, String password) {}
}

4. 新增刷新令牌接口

专门用来处理访问令牌过期后的刷新逻辑:

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refreshToken(@RequestBody RefreshTokenRequest request) {
    return refreshTokenRepository.findByToken(request.getRefreshToken())
            // 验证刷新令牌是否未过期
            .filter(token -> token.getExpiryDate().isAfter(Instant.now()))
            .map(refreshToken -> {
                User user = refreshToken.getUser();
                // 生成新的访问令牌
                String newAccessToken = jwtService.generateAccessToken(user);
                // 可选:生成新的刷新令牌,替换旧的(更安全,防止旧令牌被窃取)
                String newRefreshToken = jwtService.generateRefreshToken(user);
                refreshToken.setToken(newRefreshToken);
                refreshToken.setExpiryDate(Instant.now().plusMillis(jwtService.getRefreshExpiration()));
                refreshTokenRepository.save(refreshToken);

                return ResponseEntity.ok(new AuthResponse(newAccessToken, newRefreshToken));
            })
            .orElseThrow(() -> new RuntimeException("无效或已过期的刷新令牌"));
}

public record RefreshTokenRequest(String refreshToken) {}

5. 调整Spring Security配置,放行刷新接口

确保刷新接口不需要授权就能访问:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration {
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private final AuthenticationEntryPoint unauthorizedHandler;

    // 构造函数注入

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll() // 放行登录和刷新接口
                        .anyRequest().authenticated()
                );

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    // 保留你现有的authenticationManager、passwordEncoder等Bean
}

6. 优化JWT过滤器,处理令牌过期情况

在你的JwtAuthenticationTokenFilter里捕获访问令牌过期的异常,告诉前端去调用刷新接口:

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    // 构造函数注入

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = extractJwtFromRequest(request);

        if (StringUtils.hasText(jwt)) {
            try {
                String username = jwtService.extractUsername(jwt);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    if (jwtService.validateToken(jwt, userDetails)) {
                        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities()
                        );
                        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authToken);
                    }
                }
            } catch (ExpiredJwtException ex) {
                // 返回401并提示需要刷新令牌
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\":\"ACCESS_TOKEN_EXPIRED\",\"message\":\"请调用刷新令牌接口获取新的访问令牌\"}");
                return;
            } catch (JwtException ex) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\":\"INVALID_TOKEN\",\"message\":\"令牌无效\"}");
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    private String extractJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
三、关键安全注意事项
  1. 刷新令牌存HttpOnly Cookie:不要存在localStorage,防止XSS攻击窃取令牌。可以在登录接口里添加Cookie设置:
    Cookie refreshTokenCookie = new Cookie("refresh_token", refreshToken);
    refreshTokenCookie.setHttpOnly(true);
    refreshTokenCookie.setSecure(true); // 生产环境开启HTTPS时启用
    refreshTokenCookie.setPath("/");
    refreshTokenCookie.setMaxAge((int) (jwtService.getRefreshExpiration() / 1000));
    response.addCookie(refreshTokenCookie);
    
  2. 定期清理过期令牌:用定时任务删除数据库里过期的刷新令牌:
    @Service
    public class RefreshTokenCleanupService {
        private final RefreshTokenRepository refreshTokenRepository;
    
        @Scheduled(fixedRate = 86400000) // 每天执行一次
        public void cleanExpiredTokens() {
            refreshTokenRepository.deleteAllByExpiryDateBefore(Instant.now());
        }
    }
    
  3. 刷新令牌一次性使用:每次刷新都生成新的刷新令牌,废弃旧的,防止令牌被窃取后重复使用。
  4. 密码变更时失效刷新令牌:用户修改密码后,立即删除对应的刷新令牌,避免旧令牌还能获取新的访问令牌。

内容的提问来源于stack exchange,提问作者Vidhyasagar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 08:38:09