调整Spring Boot Security CsrfFilter顺序以解决SSO CSRF令牌无效问题
问题背景
当前过滤器链(@EnableWebSecurity(debug=true)输出)
Security filter chain: [ DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextHolderFilter HeaderWriterFilter CorsFilter CsrfFilter LogoutFilter NegotiateSecurityFilter BasicAuthenticationFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter ExceptionTranslationFilter AuthorizationFilter ]
问题现象
在无状态SSO(基于Negotiate认证)结合CSRF防护的场景下,出现异常流程:
- SSO协商将标准"Ping Pong"请求变为三次(Fiddler抓包:401、401、200/403)
- 第三次已完成认证的请求触发 "Invalid CSRF Token found" 错误
- 推测原因:
CsrfFilter在第一个请求时生成并标记令牌为已使用/从存储移除,导致SSO协商完成后令牌失效
尝试操作
通过addFilterBefore/addFilterAfter尝试调整CsrfFilter顺序至BasicAuthenticationFilter之后,但过滤器链顺序未发生变化。
当前SecurityConfig配置
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import jakarta.servlet.Filter; import waffle.spring.NegotiateSecurityFilter; import waffle.spring.NegotiateSecurityFilterEntryPoint; @Configuration @EnableWebSecurity(debug = true) @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig { @Autowired private NegotiateSecurityFilter negotiateSecurityFilter; @Autowired private NegotiateSecurityFilterEntryPoint entryPoint; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors() .and().authorizeHttpRequests().anyRequest().authenticated() .and().httpBasic().authenticationEntryPoint(entryPoint) .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and().addFilterAfter(negotiateSecurityFilter, BasicAuthenticationFilter.class); return http.build(); } }
解决方案
1. 手动注册CsrfFilter并调整顺序
Spring Security默认自动注入CsrfFilter,若要修改其执行顺序,需先禁用CSRF自动配置,再手动实例化CsrfFilter并指定插入位置:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 禁用默认CSRF自动配置 http.csrf(csrf -> csrf.disable()) .cors() .and().authorizeHttpRequests().anyRequest().authenticated() .and().httpBasic().authenticationEntryPoint(entryPoint) .and().addFilterAfter(negotiateSecurityFilter, BasicAuthenticationFilter.class); // 手动创建CsrfFilter并添加到BasicAuthenticationFilter之后 CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository); http.addFilterAfter(csrfFilter, BasicAuthenticationFilter.class); return http.build(); }
2. 调整NegotiateSecurityFilter顺序(替代方案)
若CsrfFilter顺序调整困难,可将NegotiateSecurityFilter移至CsrfFilter之前,确保认证完成后再执行CSRF校验:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors() .and().authorizeHttpRequests().anyRequest().authenticated() .and().httpBasic().authenticationEntryPoint(entryPoint) .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 将NegotiateSecurityFilter移至CsrfFilter之前 .and().addFilterBefore(negotiateSecurityFilter, CsrfFilter.class); return http.build(); }
3. 针对SSO协商请求跳过CSRF校验
针对Negotiate认证的协商请求(携带Authorization: Negotiate头)跳过CSRF校验,避免令牌在协商过程中被消耗:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors() .and().authorizeHttpRequests().anyRequest().authenticated() .and().httpBasic().authenticationEntryPoint(entryPoint) .and().csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 跳过Negotiate认证相关请求的CSRF校验 .ignoringRequestMatchers(request -> "Negotiate".equals(request.getHeader("Authorization")) ) ) .and().addFilterAfter(negotiateSecurityFilter, BasicAuthenticationFilter.class); return http.build(); }
验证调整结果
修改配置后重启应用,通过@EnableWebSecurity(debug=true)输出确认过滤器链顺序,同时用Fiddler抓包验证SSO流程的请求次数及CSRF令牌有效性。
内容的提问来源于stack exchange,提问作者Jonny
相关产品推荐
相关产品推荐

