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

无状态服务中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 13:54:50