
深度解析Spring Security与JWT双Token刷新机制的最佳实践在当今前后端分离的架构中认证授权机制的设计直接影响着用户体验和系统安全性。传统基于Session的认证方式已经无法满足现代分布式系统的需求而JWT(JSON Web Token)作为一种轻量级的认证方案因其无状态、跨域友好等特性被广泛采用。然而单纯的JWT实现往往面临一个两难选择设置较短的过期时间可以提升安全性但会导致频繁重新登录设置较长的过期时间则增加了Token泄露的风险。这正是双Token刷新机制要解决的核心问题。1. 双Token机制的设计原理与安全考量双Token机制的核心在于将认证过程分为两个层次短期的Access Token和长期的Refresh Token。Access Token用于常规API请求通常设置较短的过期时间(如15-30分钟)Refresh Token则专门用于获取新的Access Token过期时间较长(如7天)且仅能用于特定的刷新接口。这种设计带来了几个显著优势安全性提升即使Access Token被截获攻击者也只能在短时间内滥用用户体验优化用户无需频繁输入凭据系统可自动刷新Access Token细粒度控制可以独立管理两种Token的生命周期和权限关键安全考量Refresh Token必须采用与Access Token不同的存储策略每次刷新后建议使旧的Refresh Token失效(可选)需要实现完善的Token撤销机制必须防范CSRF和XSS攻击导致的Token泄露2. Spring Security集成JWT的完整配置要在Spring Security中实现双Token机制我们需要对默认的认证流程进行定制。以下是关键配置步骤2.1 基础依赖配置首先确保项目中包含必要的依赖dependencies !-- Spring Security -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- JWT支持 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies2.2 JWT工具类实现创建一个专门的JWT工具类处理Token的生成和验证public class JwtUtils { private static final String SECRET_KEY your-256-bit-secret; private static final long ACCESS_TOKEN_EXPIRE_TIME 30 * 60 * 1000; // 30分钟 private static final long REFRESH_TOKEN_EXPIRE_TIME 7 * 24 * 60 * 60 * 1000; // 7天 public static String generateAccessToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() ACCESS_TOKEN_EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String generateRefreshToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() REFRESH_TOKEN_EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } }2.3 自定义认证过滤器创建JWT认证过滤器将其插入Spring Security的过滤器链public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null JwtUtils.validateToken(token)) { Authentication authentication getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } private Authentication getAuthentication(String token) { String username JwtUtils.getUsernameFromToken(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); return new UsernamePasswordAuthenticationToken(userDetails, , userDetails.getAuthorities()); } }3. 双Token刷新机制的实现细节3.1 登录接口实现登录成功时返回双TokenRestController public class AuthController { PostMapping(/login) public ResponseEntity? login(RequestBody LoginRequest loginRequest) { Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails (UserDetails) authentication.getPrincipal(); String accessToken JwtUtils.generateAccessToken(userDetails); String refreshToken JwtUtils.generateRefreshToken(userDetails); // 可以将refreshToken存入数据库或Redis refreshTokenService.storeRefreshToken(userDetails.getUsername(), refreshToken); return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken)); } }3.2 Token刷新接口实现安全的Token刷新机制PostMapping(/refresh-token) public ResponseEntity? refreshToken(RequestBody RefreshTokenRequest refreshTokenRequest) { String refreshToken refreshTokenRequest.getRefreshToken(); if (!JwtUtils.validateToken(refreshToken)) { throw new InvalidTokenException(Refresh token is invalid or expired); } String username JwtUtils.getUsernameFromToken(refreshToken); // 验证refreshToken是否在有效存储中 if (!refreshTokenService.validateRefreshToken(username, refreshToken)) { throw new InvalidTokenException(Refresh token is not valid); } UserDetails userDetails userDetailsService.loadUserByUsername(username); String newAccessToken JwtUtils.generateAccessToken(userDetails); String newRefreshToken JwtUtils.generateRefreshToken(userDetails); // 更新存储中的refreshToken refreshTokenService.updateRefreshToken(username, refreshToken, newRefreshToken); return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); }3.3 过期Token处理自定义AuthenticationEntryPoint处理Token过期或无效的情况Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setContentType(application/json); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write( {\error\: \Unauthorized\, \message\: \Token is invalid or expired\} ); } }4. 前端集成与最佳实践4.1 Axios拦截器实现前端需要实现请求拦截和响应拦截来处理Token自动刷新// 创建axios实例 const apiClient axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, timeout: 10000 }); // 请求拦截器 apiClient.interceptors.request.use(config { const token localStorage.getItem(accessToken); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error { return Promise.reject(error); }); // 响应拦截器 apiClient.interceptors.response.use(response { return response; }, async error { const originalRequest error.config; if (error.response.status 401 !originalRequest._retry) { originalRequest._retry true; try { const refreshToken localStorage.getItem(refreshToken); const response await apiClient.post(/refresh-token, { refreshToken: refreshToken }); const { accessToken, refreshToken: newRefreshToken } response.data; localStorage.setItem(accessToken, accessToken); localStorage.setItem(refreshToken, newRefreshToken); originalRequest.headers.Authorization Bearer ${accessToken}; return apiClient(originalRequest); } catch (refreshError) { // 刷新失败跳转到登录页 localStorage.removeItem(accessToken); localStorage.removeItem(refreshToken); router.push(/login); return Promise.reject(refreshError); } } return Promise.reject(error); });4.2 Token存储策略对比存储方式安全性易用性跨标签页共享防XSS防CSRFlocalStorage中高是弱强sessionStorage中高否弱强HttpOnly Cookie高中是强需配合SameSite内存存储高低否强强提示对于高安全性要求的应用建议结合HttpOnly Cookie和内存存储使用将Refresh Token存储在HttpOnly Cookie中Access Token存储在内存中。5. 生产环境中的进阶考量5.1 Refresh Token的存储策略在生产环境中Refresh Token的存储需要特别考虑Redis存储推荐使用Redis存储Refresh Token可以方便地设置TTL和实现集群共享数据库存储关系型数据库也可行但需要考虑性能问题JWT自带过期即使存储在客户端JWT本身也有过期时间Redis存储示例Service public class RedisRefreshTokenService implements RefreshTokenService { private final RedisTemplateString, String redisTemplate; private static final String REFRESH_TOKEN_PREFIX refresh_token:; public void storeRefreshToken(String username, String refreshToken) { String key REFRESH_TOKEN_PREFIX username; redisTemplate.opsForValue().set( key, refreshToken, JwtUtils.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS ); } public boolean validateRefreshToken(String username, String refreshToken) { String key REFRESH_TOKEN_PREFIX username; String storedToken redisTemplate.opsForValue().get(key); return refreshToken.equals(storedToken); } }5.2 并发请求处理当多个请求同时遇到Token过期时需要避免重复刷新let isRefreshing false; let failedQueue []; const processQueue (error, token null) { failedQueue.forEach(prom { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue []; }; apiClient.interceptors.response.use(response { return response; }, async error { const originalRequest error.config; if (error.response.status 401 !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) { failedQueue.push({ resolve, reject }); }).then(token { originalRequest.headers.Authorization Bearer token; return apiClient(originalRequest); }).catch(err { return Promise.reject(err); }); } originalRequest._retry true; isRefreshing true; try { const refreshToken localStorage.getItem(refreshToken); const response await apiClient.post(/refresh-token, { refreshToken: refreshToken }); const { accessToken, refreshToken: newRefreshToken } response.data; localStorage.setItem(accessToken, accessToken); localStorage.setItem(refreshToken, newRefreshToken); processQueue(null, accessToken); originalRequest.headers.Authorization Bearer accessToken; return apiClient(originalRequest); } catch (refreshError) { processQueue(refreshError, null); localStorage.removeItem(accessToken); localStorage.removeItem(refreshToken); router.push(/login); return Promise.reject(refreshError); } finally { isRefreshing false; } } return Promise.reject(error); });5.3 安全增强措施Token绑定将Token与客户端指纹(如IP、User-Agent)绑定短期Refresh Token可以设置较短的Refresh Token有效期(如24小时)使用率限制限制Refresh Token的使用频率撤销机制提供管理员接口可以主动撤销TokenToken绑定示例public class EnhancedJwtUtils { public static String generateAccessToken(UserDetails userDetails, HttpServletRequest request) { String fingerprint request.getHeader(User-Agent) getClientIp(request); return Jwts.builder() .setSubject(userDetails.getUsername()) .claim(fp, DigestUtils.md5DigestAsHex(fingerprint.getBytes())) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() ACCESS_TOKEN_EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static boolean validateToken(String token, HttpServletRequest request) { try { Claims claims Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); String storedFp claims.get(fp, String.class); String currentFp DigestUtils.md5DigestAsHex( (request.getHeader(User-Agent) getClientIp(request)).getBytes() ); return storedFp.equals(currentFp); } catch (Exception e) { return false; } } private static String getClientIp(HttpServletRequest request) { String ip request.getHeader(X-Forwarded-For); if (ip null || ip.isEmpty() || unknown.equalsIgnoreCase(ip)) { ip request.getRemoteAddr(); } return ip; } }在实际项目中我们还需要考虑分布式环境下的Token验证、微服务架构下的单点登录(SSO)集成、以及如何优雅地处理用户登出等问题。双Token机制虽然增加了系统的复杂性但它为现代Web应用提供了更好的安全性和用户体验平衡。