Spring Boot Security 6集成Swagger 3:首次调用接口CSRF令牌无效
在Java 17项目中集成Spring Boot Security 6与Swagger 3,访问Swagger UI时先弹出登录窗口,登录成功进入接口概览页,这符合预期。但首次调用接口时,会再次弹出登录窗口,Spring Security日志显示“invalid CSRF token”;取消弹窗并刷新页面后,登录窗口再次出现,重新登录后回到接口概览页,此时调用接口即可成功,CSRF令牌有效。
请问为何会出现该流程?
我的安全配置类如下:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http /* * Support Javascript based applications (use json instead of HTML) * Otherwise application throws 'Invalid CSRF token' * https://docs.spring.io/spring-security/reference/6.1-SNAPSHOT/servlet/exploits/csrf.html */ .csrf((csrf) -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) ) .authorizeHttpRequests(requests -> requests .anyRequest().authenticated() ) .httpBasic(Customizer.withDefaults()); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
我通过在application.yml中配置以下属性启用Swagger 3的CSRF令牌:
springdoc: swagger-ui: csrf: enabled: true
期望实现:登录后首次通过Swagger 3调用接口即可成功。
问题原因分析
1. CSRF令牌的时机匹配问题
首次登录Swagger UI时,Spring Security会生成CSRF令牌并通过Cookie返回,但此时Swagger UI的CSRF逻辑还没完成令牌的抓取和存储——Swagger UI是在页面加载完成后才去读取Cookie中的令牌,你登录后直接调用接口时,请求里携带的令牌要么是空的,要么是旧的无效值,触发Spring Security的CSRF校验失败,进而触发重新认证流程,弹出登录窗口。
2. 刷新后的令牌同步
取消弹窗并刷新页面时,Swagger UI重新加载,会正确读取登录后Cookie里的CSRF令牌并保存,但刷新操作会重置之前的会话状态,所以需要重新登录;登录完成后,Swagger已经持有有效的令牌,后续调用接口时会自动携带,因此请求可以成功。
解决方案
方案1:让Swagger UI及时捕获登录后的CSRF令牌
自定义Swagger UI的页面脚本,在登录完成后主动从Cookie中读取XSRF-TOKEN,并设置到Swagger的全局请求头(对应X-XSRF-TOKEN)中,确保首次调用接口时就能携带有效令牌。
方案2:优化Spring Security的CSRF令牌传递逻辑
修改CsrfTokenRequestHandler的实现,确保CSRF令牌在登录响应返回时就被Swagger UI感知;或者调整CookieCsrfTokenRepository的配置,让令牌的生效时机与Swagger的加载流程匹配。
方案3:测试场景下临时放宽Swagger端点的CSRF校验(不推荐生产环境)
如果只是后端接口测试使用,可以对Swagger相关端点跳过CSRF校验:
.csrf((csrf) -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) .ignoringRequestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") )
注意:生产环境禁止此操作,会带来CSRF攻击风险。
内容的提问来源于stack exchange,提问作者DDD1

