You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Boot负载均衡场景下CSRF验证失败问题求助

这是分布式系统里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();
    }
}
方案二:用分布式共享存储统一管理CSRF令牌

如果不想自己写签名逻辑,可以用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

火山引擎 最新活动