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

Spring Security 6.2如何配置SameSite=None的CSRF Cookie?

解决方案

Spring 2.7的server.servlet.session.cookie.same-site仅作用于会话Cookie(JSESSIONID),CSRF Cookie需要通过CookieCsrfTokenRepository单独配置,同时注意SameSite=None必须配合Secure属性(Chrome要求HTTPS环境下才能生效):

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(customCookieCsrfTokenRepository())
            )
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    private CookieCsrfTokenRepository customCookieCsrfTokenRepository() {
        CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        repository.setCookiePath("/");
        repository.setCookieName("XSRF-TOKEN"); // 和Angular默认的CSRF Cookie名保持一致
        repository.setCookieMaxAge(3600);

        // 通过反射修改Cookie的SameSite属性(Spring 2.7未直接暴露该配置)
        try {
            // 确保Angular能读取到Cookie
            Field httpOnlyField = CookieCsrfTokenRepository.class.getDeclaredField("cookieHttpOnly");
            httpOnlyField.setAccessible(true);
            httpOnlyField.set(repository, false);

            // 配置CookieSerializer的SameSite和Secure属性
            Field serializerField = CookieCsrfTokenRepository.class.getDeclaredField("cookieSerializer");
            serializerField.setAccessible(true);
            DefaultCookieSerializer serializer = (DefaultCookieSerializer) serializerField.get(repository);
            serializer.setSameSite("None");
            serializer.setUseSecureCookie(true); // 必须开启,否则Chrome会拦截SameSite=None的Cookie
        } catch (Exception e) {
            throw new RuntimeException("配置CSRF Cookie SameSite失败", e);
        }
        return repository;
    }

    private CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://your-angular-domain.com")); // 替换为你的Angular域名
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true); // 必须开启,否则跨域请求不会携带Cookie
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Spring 3.x新增了CookieCsrfTokenRepository.setCookieCustomizer()方法,无需反射即可直接配置Cookie属性,更简洁可靠:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()
                    .cookieCustomizer(cookie -> {
                        cookie.sameSite("None");
                        cookie.secure(true); // 必须开启
                        cookie.path("/");
                        cookie.name("XSRF-TOKEN");
                        cookie.maxAge(3600);
                    })
                )
            )
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    // 同上的CORS配置...
}

三、修复OncePerRequestFilter丢失JSESSIONID的问题

如果之前自定义Filter时丢失了JSESSIONID的Set-Cookie头,是因为没有保留原有Cookie条目,正确逻辑是遍历所有Set-Cookie头,仅修改CSRF对应的条目:

@Component
@Order(Ordered.LOWEST_PRECEDENCE) // 确保在Spring Security过滤器之后执行
public class CsrfCookieSameSiteFilter extends OncePerRequestFilter {

    private static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);

        // 获取所有原始Set-Cookie头
        Collection<String> setCookieHeaders = response.getHeaders(HttpHeaders.SET_COOKIE);
        if (setCookieHeaders == null || setCookieHeaders.isEmpty()) {
            return;
        }

        // 清除原有头,重新添加处理后的条目
        response.setHeader(HttpHeaders.SET_COOKIE, null);
        for (String header : setCookieHeaders) {
            if (header.startsWith(CSRF_COOKIE_NAME + "=")) {
                // 为CSRF Cookie追加SameSite=None和Secure
                String updatedHeader = header + "; SameSite=None; Secure";
                response.addHeader(HttpHeaders.SET_COOKIE, updatedHeader);
            } else {
                // 保留其他Cookie头(如JSESSIONID)
                response.addHeader(HttpHeaders.SET_COOKIE, header);
            }
        }
    }
}

四、替代方案:不依赖Cookie传递CSRF Token

如果配置Cookie SameSite遇到瓶颈,可以改用请求头传递CSRF Token,完全避开Cookie的SameSite限制:

Spring Security端配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(new CsrfTokenRepository() {
                    @Override
                    public CsrfToken generateToken(HttpServletRequest request) {
                        return new DefaultCsrfToken("X-XSRF-TOKEN", "_csrf", UUID.randomUUID().toString());
                    }

                    @Override
                    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
                        // 将Token放在响应头返回,不存储到Cookie
                        response.setHeader("X-XSRF-TOKEN", token.getToken());
                    }

                    @Override
                    public CsrfToken loadToken(HttpServletRequest request) {
                        // 从请求头读取Token
                        String token = request.getHeader("X-XSRF-TOKEN");
                        return token != null ? new DefaultCsrfToken("X-XSRF-TOKEN", "_csrf", token) : null;
                    }
                })
            )
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    // 同上的CORS配置...
}

Angular端处理

通过HttpInterceptor自动获取并携带Token:

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('XSRF-TOKEN');
    if (token) {
      request = request.clone({
        setHeaders: {
          'X-XSRF-TOKEN': token
        }
      });
    }
    return next.handle(request).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          const csrfToken = event.headers.get('X-XSRF-TOKEN');
          if (csrfToken) {
            localStorage.setItem('XSRF-TOKEN', csrfToken);
          }
        }
      })
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 12:03:36