如何在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; } }
三、关键安全注意事项
- 刷新令牌存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); - 定期清理过期令牌:用定时任务删除数据库里过期的刷新令牌:
@Service public class RefreshTokenCleanupService { private final RefreshTokenRepository refreshTokenRepository; @Scheduled(fixedRate = 86400000) // 每天执行一次 public void cleanExpiredTokens() { refreshTokenRepository.deleteAllByExpiryDateBefore(Instant.now()); } } - 刷新令牌一次性使用:每次刷新都生成新的刷新令牌,废弃旧的,防止令牌被窃取后重复使用。
- 密码变更时失效刷新令牌:用户修改密码后,立即删除对应的刷新令牌,避免旧令牌还能获取新的访问令牌。
内容的提问来源于stack exchange,提问作者Vidhyasagar
相关产品推荐
相关产品推荐

