Spring Boot负载均衡场景下CSRF验证失败问题求助
这是分布式系统里CSRF防护非常常见的坑——单节点时用本地会话/密钥生成的令牌,到多节点负载均衡场景下就跨不过验证了。我给你几个生产环境验证有效的解决方案,按推荐程度排序:
核心思路是:所有服务器使用同一个加密密钥,令牌生成和验证都依赖这个共享密钥,而不是服务器本地的会话或随机密钥。这样不管请求打到哪台服务器,都能正确验证令牌。
Spring Security的CsrfTokenRepository是扩展点,你可以自定义一个基于HMAC签名的实现,或者直接用JWT来承载CSRF令牌(本质也是签名验证)。
举个自定义HMAC签名的示例:
@Component public class SharedKeyCsrfTokenRepository implements CsrfTokenRepository { // 从配置中心/环境变量注入,所有服务器用同一个值 @Value("${csrf.shared-secret}") private String sharedSecret; private final ObjectMapper objectMapper = new ObjectMapper(); @Override public CsrfToken generateToken(HttpServletRequest request) { // 生成随机令牌值 String tokenValue = UUID.randomUUID().toString(); // 用共享密钥对令牌+用户标识(比如sessionId/用户ID)签名 String signature = HmacUtils.hmacSha256Hex(sharedSecret, tokenValue + getPrincipalId(request)); // 把令牌和签名组合成最终的CSRF令牌(或者用JWT封装) String csrfToken = tokenValue + ":" + signature; return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", csrfToken); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 因为是基于签名验证,不需要存储令牌,直接返回给客户端即可 // 如果需要过期控制,可以把过期时间也签进去 } @Override public CsrfToken loadToken(HttpServletRequest request) { String token = request.getHeader("X-CSRF-TOKEN"); if (token == null) { return null; } // 拆分令牌和签名 String[] parts = token.split(":"); if (parts.length != 2) { return null; } String tokenValue = parts[0]; String signature = parts[1]; // 用共享密钥重新计算签名,验证一致性 String expectedSignature = HmacUtils.hmacSha256Hex(sharedSecret, tokenValue + getPrincipalId(request)); if (!expectedSignature.equals(signature)) { return null; } return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); } private String getPrincipalId(HttpServletRequest request) { // 获取当前用户的唯一标识,比如用户ID或sessionId(如果用共享session的话) Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth != null ? auth.getName() : request.getSession().getId(); } }
然后在Spring Security配置里替换默认的Repository:
@Configuration public class SecurityConfig { @Autowired private SharedKeyCsrfTokenRepository sharedKeyCsrfTokenRepository; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf .csrfTokenRepository(sharedKeyCsrfTokenRepository) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) ); // 其他配置... return http.build(); } }
如果不想自己写签名逻辑,可以用Redis这类分布式缓存来存储所有节点的CSRF令牌,所有服务器都从同一个Redis实例/集群读写令牌。
Spring Security已经提供了RedisCsrfTokenRepository(需要引入Spring Data Redis依赖),直接用就行:
首先引入依赖(Maven示例):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
然后配置Redis连接和CSRF Repository:
@Configuration public class SecurityConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { RedisCsrfTokenRepository csrfTokenRepository = RedisCsrfTokenRepository.withRedisConnectionFactory(redisConnectionFactory); // 设置令牌的前缀,避免和其他缓存键冲突 csrfTokenRepository.setRedisKeyPrefix("csrf:"); // 设置过期时间,比如30分钟 csrfTokenRepository.setExpiration(Duration.ofMinutes(30)); http.csrf(csrf -> csrf .csrfTokenRepository(csrfTokenRepository) ); // 其他配置... return http.build(); } }
这个方案的好处是不用自己处理签名逻辑,Spring已经封装好了,而且天然支持令牌过期清理。
如果暂时没法改代码或加分布式存储,可以让负载均衡器开启粘性会话(Sticky Session),把同一个用户的所有请求都路由到同一台服务器。这样用户的CSRF令牌只存在该服务器的本地会话里,就能正常验证。
但这个方案有明显缺点:
- 容错性差:如果该服务器宕机,用户的会话(包括CSRF令牌)就丢失了,需要重新登录/生成令牌
- 不利于扩展:无法做到请求的均匀分发,部分服务器可能负载过高
所以只适合临时应急,长期还是推荐前两个方案。
- 共享密钥一定要安全存储:绝对不能硬编码在代码里,要用环境变量、Spring Cloud Config、KMS等方式管理,避免泄露
- 令牌过期控制:不管用签名还是Redis存储,都要设置合理的过期时间,降低令牌泄露后的风险
- 负载均衡器配置:确保负载均衡器不会过滤掉你的CSRF请求头(比如
X-CSRF-TOKEN),要把这些头正确转发到后端服务器
内容的提问来源于stack exchange,提问作者iamrajshah




