Spring 3升级后CSRF令牌刷新失效,请求仍返回403问题求助
问题
我们生产环境的应用采用以下逻辑刷新CSRF令牌,该逻辑在Spring 2中可正常运行,核心是CSRF令牌过期时静默刷新以避免中断会话。但升级至Spring 3后,初始令牌能正常设置,刷新令牌后的请求仍返回403,且初始有效令牌与刷新后的令牌格式差异显著:
后端代码
private static final String CSRF_TOKEN_SETTER = "window.import.meta.env.CSRF_TOKEN=\"%s\";"; // 从请求设置初始令牌 @GetMapping(value = "/csrf.js", produces = "application/javascript; charset=utf-8") public String csrf(HttpServletRequest request) { if (request.getAttribute("_csrf") instanceof CsrfToken token) { return String.format(CSRF_TOKEN_SETTER, token.getToken()); } return "ERROR"; } // Spring升级后失效的刷新接口 @GetMapping(value = "/refresh_csrf_token") public ResponseEntity<String> refreshCsrfToken(HttpServletRequest request, HttpServletResponse response) { CsrfToken newToken = csrfTokenRepository.generateToken(request); csrfTokenRepository.saveToken(newToken, request, response); return ResponseEntity.ok(newToken.getToken()); }
前端逻辑
if (statusCodeIs403) { refreshCsrfTokenAndAppendToHeadersOfFutureRequests(); }
令牌格式对比
- 有效初始令牌:
sBkVwbeM-hgCeILgJFU6zADrNZkMpcnkG3i0vzqSMIR6MWis1Xgk9Y-6mCkvT-DWE3gO9WPeGPs4xvrJKUqCjQ7wCLEZVF6Z - 刷新后令牌:
3c78e076-9431-4f70-97a8-0aa8760037a7
CsrfTokenRepository配置
@Bean @Lazy public CsrfTokenRepository csrfTokenRepository() { HttpSessionCsrfTokenRepository tokenRepository = new HttpSessionCsrfTokenRepository(); tokenRepository.setHeaderName("X-CSRF-TOKEN"); return tokenRepository; }
请问是否遗漏令牌保存步骤?或是Spring 3的哪些变化导致了该问题?
分析与解决方案
核心问题原因
Spring 3对应的Spring Security 6.x版本对HttpSessionCsrfTokenRepository做了两处关键变更,直接导致你的刷新逻辑失效:
- 令牌生成规则变更:旧版本默认用
SecureRandom生成长字符串令牌,新版本默认生成UUID格式令牌,这就是两种格式差异的原因,但格式本身不是403的直接诱因。 - 令牌验证绑定规则严格化:
- 刷新接口用
GET请求:Spring Security默认忽略GET请求的CSRF验证,导致生成新令牌时,会话上下文未正确初始化,新令牌无法与当前会话建立绑定关系。 - 未验证旧令牌直接生成新令牌:Spring Security 6要求刷新令牌前必须先验证旧令牌的有效性,直接生成并保存的新令牌,不会被后续请求的验证逻辑认可。
- 刷新接口用
修复步骤
将刷新接口改为POST请求
只有POST请求会触发CSRF验证流程,确保会话与令牌上下文正确关联:@PostMapping(value = "/refresh_csrf_token") public ResponseEntity<String> refreshCsrfToken(HttpServletRequest request, HttpServletResponse response) { // 先读取旧令牌,Spring Security会自动完成有效性验证,无效则直接返回403 CsrfToken existingToken = csrfTokenRepository.loadToken(request); if (existingToken == null) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid session"); } // 生成并保存新令牌 CsrfToken newToken = csrfTokenRepository.generateToken(request); csrfTokenRepository.saveToken(newToken, request, response); return ResponseEntity.ok(newToken.getToken()); }前端请求刷新接口时携带当前令牌
调用刷新接口必须在请求头中带上当前有效的X-CSRF-TOKEN,否则Spring Security会拦截该POST请求:async function refreshCsrfTokenAndAppendToHeadersOfFutureRequests() { const currentToken = window.import.meta.env.CSRF_TOKEN; const response = await fetch('/refresh_csrf_token', { method: 'POST', headers: { 'X-CSRF-TOKEN': currentToken, 'Content-Type': 'application/json' } }); if (response.ok) { window.import.meta.env.CSRF_TOKEN = await response.text(); } }保持令牌存储键默认值
HttpSessionCsrfTokenRepository默认的会话存储键为org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN,不要自定义该键,否则验证时无法读取到新令牌。
可选:恢复旧令牌格式
如果需要保持旧版本的长字符串令牌格式,可以自定义令牌生成器:
@Bean public CsrfTokenRepository csrfTokenRepository() { HttpSessionCsrfTokenRepository tokenRepository = new HttpSessionCsrfTokenRepository(); tokenRepository.setHeaderName("X-CSRF-TOKEN"); // 自定义令牌生成逻辑,生成与旧版本一致的长字符串 tokenRepository.setTokenGenerator(() -> { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[32]; random.nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); }); return tokenRepository; }
内容的提问来源于stack exchange,提问作者SamPinheiro
相关产品推荐
相关产品推荐

