You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cloud Gateway配置CSRF保护遇"未找到预期CSRF令牌"错误求助

Spring Cloud Gateway中CSRF保护验证失败问题

我在Spring Cloud Gateway中配置CSRF保护时,浏览器已成功获取到XSRF-TOKEN Cookie,但将该Cookie值通过请求头X-XSRF-TOKEN: somevalue携带发送请求时,始终收到错误:An expected CSRF token cannot be found。

安全配置代码

import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.CsrfToken;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {

  @Bean
  public SecurityWebFilterChain springSecurityFilterChain(
      ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {

    http.csrf(
            csrfSpec ->
                csrfSpec
                    .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
                    .csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()))
        .authorizeExchange(
            authorizeExchangeSpec ->
                authorizeExchangeSpec
                    .anyExchange()
                    .authenticated())
       
        .oauth2Login(Customizer.withDefaults())
        .logout();
    return http.build();
  }

  @Bean
  WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
      exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe();
      return chain.filter(exchange);
    };
  }
}

SpaServerCsrfTokenRequestHandler代码

import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {

  private final ServerCsrfTokenRequestAttributeHandler delegate =
      new XorServerCsrfTokenRequestAttributeHandler();

  @Override
  public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
    this.delegate.handle(exchange, csrfToken);
  }

  @Override
  public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
    if (exchange.getRequest().getHeaders().getOrEmpty(csrfToken.getHeaderName()).stream()
        .anyMatch(StringUtils::hasText)) {
      return super.resolveCsrfTokenValue(exchange, csrfToken);
    }
    return this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
  }
}

参考了Spring Security 6 CSRF保护失效问题的相关讨论以及官方文档中SPA应用的CSRF集成指南。


问题排查与解决方案

1. 核心问题:Token加密逻辑不匹配

XorServerCsrfTokenRequestAttributeHandler会对Cookie中的CSRF Token做异或加密,浏览器拿到的XSRF-TOKEN是加密后的值。你的请求处理器逻辑是:当请求头存在X-XSRF-TOKEN时直接读取明文验证,但存储的Token是加密后的,自然匹配失败。

2. 修正请求处理器逻辑

修改SpaServerCsrfTokenRequestHandler,确保请求头传入的加密Token能被正确解密验证:

import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {

    private final XorServerCsrfTokenRequestAttributeHandler delegate =
            new XorServerCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
        this.delegate.handle(exchange, csrfToken);
    }

    @Override
    public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
        String headerToken = exchange.getRequest().getHeaders().getFirst(csrfToken.getHeaderName());
        // 优先读取请求头,并用delegate统一解密
        if (StringUtils.hasText(headerToken)) {
            return this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
        }
        return this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
    }
}

3. 优化CsrfCookieWebFilter的异步处理

原代码直接subscribe()可能导致Token未加载完成就进入后续链路,改用then确保异步操作完成:

@Bean
WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
        Mono<CsrfToken> csrfTokenMono = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
        return csrfTokenMono.then(chain.filter(exchange));
    };
}

4. 检查跨域配置(若存在跨域场景)

如果前端和网关跨域部署,需确保CORS允许携带X-XSRF-TOKEN请求头和XSRF-TOKEN Cookie:

@Bean
CorsWebFilter corsWebFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOriginPattern("*"); // 替换为实际允许的域名
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsWebFilter(source);
}

5. 验证请求头名称一致性

确认请求头名称与CsrfToken.getHeaderName()返回值一致,默认是X-XSRF-TOKEN,避免拼写错误。


内容的提问来源于stack exchange,提问作者vishal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 16:25:28