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
相关产品推荐
相关产品推荐

