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

Spring Boot3.2迁移后表单POST下载文件报CSRF Token null错误

Spring Boot 2.7 迁移至3.2后,表单提交方式下载文件出现CSRF Token为空错误

我将Spring Boot应用从2.7版本迁移到3.2版本后,Vue.js结合Axios的登录及各类请求均正常,但通过JavaScript创建表单元素发起POST请求下载文件(使用Puppeteer生成页面PDF)时,出现CSRF Token为null的错误,无法定位安全配置中的问题。

错误信息

{
"title":"Something Went Wrong",
"status":500,
"detail":"Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."
}

请求Payload示例

values:{
"htmlContent":"用于生成PDF的HTML内容",
"config":{}
}
_csrf: csrf令牌
t: 某个整数值

原文件下载的JavaScript方法

function downloadFile(url, method, data, transactionId) {
const form = createElement('FORM');
form.method = method;
form.action = url;
const input = createElement('INPUT');
input.id = 'values';
input.name = 'values';
input.type = 'hidden';
input.value = JSON.stringify(data);
form.appendChild(input);

const csrfInput = createElement('INPUT');
csrfInput.id = '_csrf';
csrfInput.name = '_csrf';
csrfInput.type = 'hidden';

debugger;
csrfInput.value = Cookies.get('XSRF-TOKEN');
form.appendChild(csrfInput);

if (transactionId) {
    const transactionIdInput = createElement('INPUT');
    transactionIdInput.id = 't';
    transactionIdInput.name = 't';
    transactionIdInput.type = 'hidden';
    transactionIdInput.value = transactionId;
    form.appendChild(transactionIdInput);
}

document.body.appendChild(form);
form.submit();
form.parentNode.removeChild(form);
}

参考Spring文档实现的CSRF Token请求处理器

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
    private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
        /*
         * 始终使用XorCsrfTokenRequestAttributeHandler为响应体中的CsrfToken提供BREACH防护
         */
        this.delegate.handle(request, response, csrfToken);
    }

    @Override
    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        /*
         * 如果请求包含请求头,使用CsrfTokenRequestAttributeHandler解析CsrfToken。
         * 适用于单页应用自动包含从原始CsrfToken Cookie获取的头值的场景。
         */
        if (StringUtils.isNotBlank(request.getHeader(csrfToken.getHeaderName()))) {
            return super.resolveCsrfTokenValue(request, csrfToken);
        }
        /*
         * 其他情况(比如请求包含请求参数),使用XorCsrfTokenRequestAttributeHandler解析CsrfToken。
         * 适用于服务端渲染表单将_csrf请求参数作为隐藏输入的场景。
         */
        return this.delegate.resolveCsrfTokenValue(request, csrfToken);
    }
}

final class CsrfCookieFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
        // 通过加载延迟令牌,将令牌值渲染到Cookie中
        csrfToken.getToken();

        filterChain.doFilter(request, response);
    }
}

安全配置代码

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(management -> management.maximumSessions(-1).sessionRegistry(sessionRegistry()).expiredUrl("/api/expire-me"))
            .csrf(csrf -> {
                CookieCsrfTokenRepository csrfRepo = CookieCsrfTokenRepository.withHttpOnlyFalse();
                SpaCsrfTokenRequestHandler requestHandler = new SpaCsrfTokenRequestHandler();
                csrf.requireCsrfProtectionMatcher(new CsrfSecurityRequestMatcher()).csrfTokenRepository(csrfRepo).csrfTokenRequestHandler(requestHandler);
            }).addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)...
}

补充说明

使用Fetch方法可以正常下载,因为该方法可将CSRF Token附加到请求头中,对应的JavaScript方法如下:

function downloadFile(url, method, data, transactionId) {
const headers = new Headers({
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-XSRF-TOKEN': Cookies.get('XSRF-TOKEN')
});
const reqParams = new URLSearchParams();
reqParams.append('t', transactionId);
reqParams.append('values', JSON.stringify(data));
fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    credentials: 'same-origin', // include, *same-origin, omit
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'strict-origin-when-cross-origin', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: reqParams, // body数据类型必须匹配"Content-Type"头
    headers: headers
})
    .then((response) => response.blob())
    .then((blob) => {
        // 创建Blob URL
        const blobUrl = window.URL.createObjectURL(blob);

        // 创建临时锚点元素
        const anchorEl = document.createElement('a');
        anchorEl.href = blobUrl;
        anchorEl.download = `history.pdf`; // 设置文件名和扩展名
        anchorEl.style.display = 'none';

        // 将锚点元素添加到DOM并点击触发下载
        document.body.appendChild(anchorEl);
        anchorEl.click();

        // 清理资源
        document.body.removeChild(anchorEl);
        window.URL.revokeObjectURL(blobUrl);
    })
    .catch((error) => {
        console.error('文件下载出错:', error);
    });
}

内容的提问来源于stack exchange,提问作者aakash singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 12:37:55