Spring Security 6.2如何配置SameSite=None的CSRF Cookie?
解决方案
一、Spring 2.7 配置CSRF Cookie SameSite=None
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 配置CSRF Cookie SameSite=None
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
相关产品推荐
相关产品推荐

