无状态服务中SpringDoc集成CSRF异常及Swagger优化需求
解决方案:解决Swagger UI首次CSRF请求403问题
针对你遇到的无状态服务下条件性CSRF配置导致Swagger UI首次修改请求403的问题,提供以下几种可行方案:
方案一:预获取CSRF Token并自动填充到Swagger UI
通过在Swagger UI加载时主动获取CSRF Token,避免首次请求因无Token而返回403。
1. 添加CSRF Token获取接口
创建一个允许已认证人类用户访问的接口,用于获取CSRF Token:
@RestController @RequestMapping("/api/csrf") public class CsrfController { @GetMapping public CsrfToken getCsrfToken(HttpServletRequest request) { return (CsrfToken) request.getAttribute(CsrfToken.class.getName()); } }
2. 配置Spring Security授权规则
在SecurityFilterChain中允许访问该接口及Swagger相关路径:
.authorizeHttpRequests(r -> r .requestMatchers("/api/csrf", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // 其他业务接口授权规则 )
3. 添加Swagger UI自定义脚本
在application.yml中配置Swagger UI加载自定义脚本:
springdoc: show-actuator: true swagger-ui: csrf: enabled: true custom-scripts: - /js/csrf-handler.js
创建src/main/resources/static/js/csrf-handler.js,实现自动获取Token并添加到请求头:
document.addEventListener('DOMContentLoaded', function() { // 调用CSRF接口获取Token fetch('/api/csrf', { credentials: 'include' }) .then(response => response.json()) .then(data => { const ui = window.ui; if (!ui) return; // 添加请求拦截器,为修改型请求自动带上CSRF Header ui.serverActions.addRequestInterceptor((request) => { const modifyMethods = ['PUT', 'PATCH', 'POST', 'DELETE']; if (modifyMethods.includes(request.method.toUpperCase())) { request.headers[data.headerName] = data.token; } return request; }); }) .catch(err => console.error('获取CSRF Token失败:', err)); });
方案二:修复SwaggerIndexPageTransformer并添加请求重试逻辑
解决你之前注入脚本导致Swagger UI空白的问题,同时实现403自动重试。
1. 修正Transformer的HTML修改逻辑
避免使用Jsoup解析破坏Swagger原有结构,直接在</body>前追加脚本:
@Bean public SwaggerIndexPageTransformer customSwaggerIndexPageTransformer(SwaggerUiConfigProperties swaggerUiConfigProperties, SwaggerUiOauthProperties uiOauthProperties, SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider) { return new SwaggerIndexPageTransformer(swaggerUiConfigProperties, uiOauthProperties, swaggerWelcomeCommon, objectMapperProvider) { @Override public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException { String html = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); // 在</body>标签前插入自定义脚本 String scriptTag = "<script src='/interceptors/csrf.js' charset='UTF-8'></script>"; html = html.replace("</body>", scriptTag + "</body>"); return new TransformedResource(resource, html.getBytes(StandardCharsets.UTF_8)); } }; }
2. 编写重试逻辑脚本
创建src/main/resources/static/interceptors/csrf.js,拦截请求并处理403重试:
const originalFetch = window.fetch; window.fetch = function(resource, options) { return originalFetch(resource, options) .then(response => { const modifyMethods = ['PUT', 'PATCH', 'POST', 'DELETE']; if (response.status === 403 && options.method && modifyMethods.includes(options.method.toUpperCase())) { // 从Cookie中提取CSRF Token(需解码XOR加密内容) const csrfCookie = document.cookie.split('; ') .find(row => row.startsWith('XSRF-TOKEN='))?.split('=')[1]; if (!csrfCookie) return response; const decodedToken = decodeURIComponent(csrfCookie); // 重新构造请求,添加CSRF Header const newOptions = {...options}; newOptions.headers = {...newOptions.headers, 'X-XSRF-TOKEN': decodedToken}; // 重试请求 return originalFetch(resource, newOptions); } return response; }); };
注:因你使用了
XorCsrfTokenRequestAttributeHandler,Cookie中的Token经过XOR编码,需解码后才能用于请求头。
方案三:调整Spring Security的CSRF Token生成时机
在用户认证成功后主动生成CSRF Token并设置Cookie,确保首次请求时已有可用Token。
修改SecurityFilterChain配置,添加自定义过滤器在认证后生成Token:
@Bean public SecurityFilterChain securityFilterchain(HttpSecurity httpSecurity) throws Exception { CsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); ((CookieCsrfTokenRepository) repository).setCookieCustomizer(c -> c .secure(true) .sameSite("Strict")); XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler(); // 自定义过滤器:认证后为人类用户生成CSRF Token Filter csrfTokenGeneratorFilter = (request, response, chain) -> { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof MyUser user && !user.isSystem) { CsrfToken token = repository.generateToken(request); repository.saveToken(token, request, response); request.setAttribute(CsrfToken.class.getName(), token); } chain.doFilter(request, response); }; httpSecurity .csrf(customizer -> customizer .requireCsrfProtectionMatcher(request -> { Set<String> requireCsrf = Set.of("PUT", "PATCH", "POST", "DELETE"); if (!requireCsrf.contains(request.getMethod())) return false; Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) return false; MyUser user = (MyUser) auth.getPrincipal(); return !user.isSystem; }) .csrfTokenRepository(repository) .csrfTokenRequestHandler(delegate::handle)) .sessionManagement(smc -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(authFilter(), CsrfFilter.class) .addFilterAfter(csrfTokenGeneratorFilter, AuthFilter.class) // 替换为你的认证过滤器类名 .authorizeHttpRequests(r -> r .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 其他授权规则 ); return httpSecurity.build(); }
方案推荐
- 优先选择方案一:用户体验最优,通过预获取Token彻底避免403重试,逻辑清晰易维护。
- 若不想新增接口,可选择方案三:从Spring Security层面调整Token生成时机,根源解决问题。
- 方案二适合需要保留重试逻辑的场景,但需注意XOR解码的兼容性。
内容的提问来源于stack exchange,提问作者Display name
相关产品推荐
相关产品推荐

