Spring 转发 form-data 文件上传请求时中文文件名乱码

Spring 转发 form-data 文件上传请求时中文文件名乱码

    • 复现问题
    • 找原因
    • 解决问题
    • 参考

复现问题

后端有两个接口:

/upload 是文件上传的接口。

/forward 是转发文件上传请求的接口。

@RequestMapping
@RestController
public class FileUploadController {/*** 直接调用-文件上传*/@PostMapping("/upload")public String upload(HttpServletRequest request, @RequestParam("title") String title,@RequestParam("file") MultipartFile file) {return """title: %s <br/>filename: %s <br/>contentType: %s""".formatted(title, file.getOriginalFilename(), file.getContentType());}/*** 转发请求*/@PostMapping("/forward")public String forward(HttpServletRequest request) throws ServletException, IOException {try (CloseableHttpClient httpclient = HttpClients.createDefault()) {ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.post("http://localhost:8080/upload");MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();for (Part part : request.getParts()) {MultipartPartBuilder partBuilder = MultipartPartBuilder.create();for (String headerName : part.getHeaderNames()) {partBuilder.addHeader(headerName, part.getHeader(headerName));}InputStreamBody body = new InputStreamBody(part.getInputStream(), part.getSubmittedFileName());partBuilder.setBody(body);entityBuilder.addPart(partBuilder.build());}ClassicHttpRequest forwardRequest = requestBuilder.setEntity(entityBuilder.build()).build();return httpclient.execute(forwardRequest, response -> {final HttpEntity responseEntity = response.getEntity();String res = EntityUtils.toString(responseEntity);EntityUtils.consume(responseEntity);return res;});}}
}

前端使用 form 进行文件上传

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>test</title>
</head>
<body><h2>直接调用</h2><form action="/upload" method="post" enctype="multipart/form-data"><label>标题:</label><input type="text" name="title" value="直接调用" required/><label>文件:</label><input type="file" name="file" required/><input type="submit" value="提交"></form><h2>转发请求</h2><form action="/forward" method="post" enctype="multipart/form-data"><label>标题:</label><input type="text" name="title" value="转发请求" required/><label>文件:</label><input type="file" name="file" required/><input type="submit" value="转发"></form>
</body>
</html>

image-20250430104419317

测试发现直接调用时,文件名正常显示,但转发请求时却乱码了。

image-20250430104908735

找原因

通过 Apifox 调用 /forward 发现不会乱码。

image-20250430105221852

使用 fiddler 抓包,浏览器和 Apifox 请求 /forward 的数据包。发现 Apifox 在 multipart body 的 Content-Disposition header 中多了一个 filename* 的属性。

image-20250430105608060

Content-Disposition 的文档中这样写到:

  • filename

    后面是要传送的文件的初始名称的字符串。这个参数总是可选的,而且不能盲目使用:路径信息必须舍掉,同时要进行一定的转换以符合服务器文件系统规则。这个参数主要用来提供展示性信息。当与 Content-Disposition: attachment 一同使用的时候,它被用作"保存为"对话框中呈现给用户的默认文件名。

  • filename\*

    filenamefilename* 两个参数的唯一区别在于,filename* 采用了 RFC 5987 中规定的编码方式。当 filenamefilename* 同时出现的时候,应该优先采用 filename*,假如二者都支持的话。

filename* 的优先级高于 filename

当使用 multipart/form-data 格式提交表单数据时,每个子部分(例如每个表单字段和任何与字段数据相关的文件)都需要提供一个 Content-Disposition 标头,以提供相关信息。标头的第一个指令始终为 form-data,并且还必须包含一个 name 参数来标识相关字段。额外的指令不区分大小写,并使用带引号的字符串语法在 = 号后面指定参数。多个参数之间使用分号(;)分隔。

Content-Disposition: form-data; name="fieldName"
Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"

上述 Content-Disposition 的文档中也指出了,要添加多个参数,需要用 ; 分隔。

下一步就是要弄明白 filename* 的值应该怎么构造。以下是 RFC 5987 中关于参数名、参数值的语法,其中又提到了 RFC 2231、RFC 3986, 比较晦涩难懂:

  parameter     = reg-parameter / ext-parameterreg-parameter = parmname LWSP "=" LWSP valueext-parameter = parmname "*" LWSP "=" LWSP ext-valueparmname      = 1*attr-charext-value     = charset  "'" [ language ] "'" value-chars; like RFC 2231's <extended-initial-value>; (see [RFC2231], Section 7)charset       = "UTF-8" / "ISO-8859-1" / mime-charsetmime-charset  = 1*mime-charsetcmime-charsetc = ALPHA / DIGIT/ "!" / "#" / "$" / "%" / "&"/ "+" / "-" / "^" / "_" / "`"/ "{" / "}" / "~"; as <mime-charset> in Section 2.3 of [RFC2978]; except that the single quote is not included; SHOULD be registered in the IANA charset registrylanguage      = <Language-Tag, defined in [RFC5646], Section 2.1>value-chars   = *( pct-encoded / attr-char )pct-encoded   = "%" HEXDIG HEXDIG; see [RFC3986], Section 2.1attr-char     = ALPHA / DIGIT/ "!" / "#" / "$" / "&" / "+" / "-" / "."/ "^" / "_" / "`" / "|" / "~"; token except ( "*" / "'" / "%" )

总结后就是:

// charset 字符集,比如 UTF-8、ISO-8859-1
// language 语言,比如 en,可以省略,language 的前后使用单引号分隔
// percentEncoding 百分号编码,% 加上两个 16 进制,比如中的编码为 %E4%B8%AD,我们常见的空格的编码为 %20
// rawStr 原始的字符串,比如 中文 abc 123.png
parmname + "*" + "=" + charset + "'" + language + "'" + percentEncoding(rawStr)

js 相关

注意 encodeURIComponent 并未将 百分号编码 | MDN 中描述的所有特殊字符进行编码,在 encodeURIComponent() - JavaScript | MDN 的描述中提到它不会编码 ! * ' ( ) 这五个字符,所以需要特殊处理。

百分号编码 | MDN 还提到了以下内容,这对后续 java 后端添加 filename* 很重要。

根据上下文,空白符 ' ' 将会转换为 '+' (如使用百分号编码的 application/x-www-form-urlencoded 消息),或者将会转换为 '%20'(如 URL 中)。

翻译成 js 代码是:

// 例如 filename*=UTF-8''%E4%B8%AD%E6%96%87%20abc%20123.png
parmname = "filename"
charset = "UTF-8"
language = ""
rawStr = "中文 abc 123.png"function percentEncoding(rawStr) {var res = encodeURIComponent(rawStr)// 特殊处理 ! * ' ( )res = res.replace(/\*/g, '%2A')res = res.replace(/!/g, '%21')res = res.replace(/\(/g, '%28')res = res.replace(/\)/g, '%29')res = res.replace(/'/g, '%27')return res
}
// 重点
kv = `${parmname}*=${charset}'${language}'${percentEncoding(rawStr)}`

image-20250430144427345

解决问题

解决问题最直接的解决办法就是前端上传时在 Content-Disposition 中添加 ; filename*=UTF-8''%E4%B8%AD%E6%96%87%20abc%20123.png 这一段。但查阅资料后发现 <form> 标签不支持直接控制 Content-Disposition 的值。

最后采用后端转发时添加 filename* 的方式。

/*** 转发请求*/
@PostMapping("/forward")
public String forward(HttpServletRequest request) throws ServletException, IOException {try (CloseableHttpClient httpclient = HttpClients.createDefault()) {ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.post("http://localhost:8080/upload");MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();for (Part part : request.getParts()) {MultipartPartBuilder partBuilder = MultipartPartBuilder.create();for (String headerName : part.getHeaderNames()) {String headerValue = part.getHeader(headerName);// 如果是文件上传,则为 Content-Disposition 添加 filename*if ("Content-Disposition".equalsIgnoreCase(headerName)&& headerValue.contains("filename")&& !headerValue.contains("filename*")) {headerValue += "; filename*=UTF-8''%s".formatted(percentEncoding(part.getSubmittedFileName()));}partBuilder.addHeader(headerName, headerValue);}InputStreamBody body = new InputStreamBody(part.getInputStream(), part.getSubmittedFileName());partBuilder.setBody(body);entityBuilder.addPart(partBuilder.build());}ClassicHttpRequest forwardRequest = requestBuilder.setEntity(entityBuilder.build()).build();return httpclient.execute(forwardRequest, response -> {final HttpEntity responseEntity = response.getEntity();String res = EntityUtils.toString(responseEntity);EntityUtils.consume(responseEntity);return res;});}
}private static String percentEncoding(String raw) {// 在 encode 方法的注释中能看出,encode 方法遵循 application/x-www-form-urlencoded 的规范,// 会将空格替换为 +,所以需要再次将 + 替换为 %20return URLEncoder.encode(raw, StandardCharsets.UTF_8).replace("+", "%20");
}

乱码问题完美解决:

image-20250430150335568

参考

  • Content-Disposition - HTTP | MDN | en-US
  • Content-Disposition - HTTP | MDN | zh-CN
  • RFC 5987 - Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters
  • RFC 2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations
  • 百分号编码 - MDN Web 文档术语表:Web 相关术语的定义 | MDN
  • encodeURIComponent() - JavaScript | MDN
  • RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
  • 【编码篇】看破字符 %20 之谜,百分号编码以及其背后前言 提到这个 %20,想必大家都见过,熟悉一点编码的人,还会知道 - 掘金
  • 下载的附件名总乱码?你该去读一下 RFC 文档了! - 郑晓龙 - 博客园

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/81738.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 8.4.4 安全升级指南:从漏洞修复到版本升级全流程解析

目录 二、升级前关键注意事项 1. 数据安全与备份 2. 版本兼容性与路径规划 三、分步升级操作流程 1. 环境预检与准备 2. 安装包部署 3. 强制升级组件 4. 验证与启动 一、背景与必要性 近期安全扫描发现生产环境的 MySQL 数据库存在多个高危漏洞(CVE 详情参见Oracle 官…

vulkanscenegraph显示倾斜模型(6.4)-多线程下的记录与提交

前言 上章深入分析了帧循环中呈现阶段的具体实现。本章将分析多线程下的记录与提交&#xff0c;进一步剖析vsg帧循环过程中的同步机制&#xff0c;并揭露信号量(VkSemaphore)和围栏(VkFence)以及vsg::FrameBlock与vsg::Barrier在其中的作用。 目录 1 信号量(VkSemaphore)、栅栏…

Python爬虫实战:获取扇贝单词数据并分析,为用户高效学习单词做参考

一、引言 随着互联网的迅猛发展,在线学习资源日益丰富多样。扇贝单词作为一款备受欢迎的在线英语学习平台,积累了海量的单词学习数据。借助 Python 强大的爬虫技术获取这些数据,并运用数据分析和机器学习方法进行深度挖掘,能够为用户量身定制更个性化、更高效的单词学习方…

【Vagrant+VirtualBox创建自动化虚拟环境】Ansible-Playbook

Vagrant 后续Ansible实战&#xff1a;【Ansible自动化运维实战&#xff1a;从Playbook到负载均衡指南】-CSDN博客 Vagrant是一个基于Ruby的工具&#xff0c;用于创建和部署虚拟化开发环境。它使用Oracle的开源VirtualBox虚拟化系统&#xff0c;使用 Chef创建自动化虚拟环境 Do…

Codigger Desktop:重新定义数字工作与生活方式

Codigger Desktop是一款革命性的智能桌面操作系统&#xff0c;专为现代数字生活和工作场景打造。它不仅成为开发者的强大生产力工具&#xff0c;更是普通用户日常数字生活的得力助手&#xff0c;完美实现了专业性与易用性的平衡。 Multimedia Desktop全能数字生活平台 重新定…

Servlet+tomcat

serverlet 定义&#xff1a;是一个接口&#xff0c;定义了java类被浏览器&#xff08;tomcat识别&#xff09;的规则 所以我们需要自定义一个类&#xff0c;实现severlet接口复写方法 通过配置类实现路径和servlet的对应关系 执行原理 当用户在浏览器输入路径&#xff0c;会…

什么是 DDoS 攻击?高防 IP 如何有效防护?2025全面解析与方案推荐

一、DDoS 攻击&#xff1a;互联网时代的 “数字核武器” 1. DDoS 攻击的本质与原理 ** 分布式拒绝服务攻击&#xff08;DDoS&#xff09;** 通过操控海量僵尸设备&#xff0c;向目标服务器发送洪水般请求&#xff0c;耗尽带宽、连接或计算资源&#xff0c;导致合法用户无法访…

Circular Plot系列(一): 环形热图绘制

针对近期多个粉丝咨询环形图的绘制&#xff0c;我意识到&#xff0c;我们似乎没有真正介绍过circle图&#xff0c;但这一类图确是非常常用的图&#xff0c;所以这里详细学习一下circle的绘制&#xff0c;使用的是circlize包&#xff0c;功能很完善&#xff1a;安装包, #https:/…

【数据挖掘】时间序列预测-时间序列预测策略

时间序列预测策略 &#xff08;1&#xff09;单步预测与多步预测&#xff08;2&#xff09;直接多步预测&#xff08;3&#xff09;递归多步预测&#xff08;4&#xff09;直接递归的混合预测&#xff08;5&#xff09;多输入多输出预测 &#xff08;1&#xff09;单步预测与多…

【LLM】deepseek R1之GRPO训练笔记(持续更新)

note 相关框架对比&#xff1a; 需微调模型且资源有限 → Unsloth&#xff1b;本地隐私优先的小规模推理 → Ollama&#xff1b;复杂逻辑或多模态任务 → SGLang&#xff1b;高并发生产环境 → vLLM 微调SFT和GRPO是确实能学到新知识的四种格式&#xff08;messages、sharegpt…

【数据结构】--- 单链表的增删查改

前言&#xff1a; 经过了几个月的漫长岁月&#xff0c;回头时年迈的小编发现&#xff0c;数据结构的内容还没有写博客&#xff0c;于是小编赶紧停下手头的活动&#xff0c;补上博客以洗清身上的罪孽 目录 前言 概念&#xff1a; 单链表的结构 我们设定一个哨兵位头节点给链…

【JAVA】数据类型与变量:深入理解栈内存分配(4)

核心知识点详细解释 Java 的基本数据类型和引用数据类型 基本数据类型 Java 有 8 种基本数据类型&#xff0c;它们可以分为 4 类&#xff1a; 整数类型&#xff1a;byte&#xff08;1 字节&#xff09;、short&#xff08;2 字节&#xff09;、int&#xff08;4 字节&#…

ReentrantLock实现公平锁和非公平锁

在 Java 里&#xff0c;公平锁和非公平锁是多线程编程中用于同步的两种锁机制&#xff0c;它们的主要差异在于获取锁的顺序规则。下面是对二者的详细介绍&#xff1a; 公平锁 公平锁遵循 “先来先服务” 原则&#xff0c;也就是线程获取锁的顺序和请求锁的顺序一致。先请求锁…

一篇撸清 Http,SSE 与 WebSocket

HTTP,SSE 和WebSocket都是网络传输的协议,本篇快速介绍三者的概念和比较。 SSE(Server-Sent Events) 是什么? SSE(Server-Sent Events),服务器发送事件, 是一种基于 HTTP 的轻量级协议,允许服务器主动向客户端(如浏览器)推送实时数据。它设计用于单向通信(服务器到…

5个重要的财务指标讲解

1&#xff09;净资产收益率 2&#xff09;销售净利率 3&#xff09; 销售毛利率 4&#xff09;销售成本率 5&#xff09; 期间费用率 好的&#xff0c;我将通过一个假设的案例&#xff08;某公司2023年数据&#xff09;逐步解释这些财务指标&#xff0c;并用具体数字演示计算…

PISI:眼图1:眼图相关基本概念

0 英文缩写 TIE&#xff08;Time Interval Error&#xff09;时间间隔误差&#xff0c;UI&#xff08;Unit Interval&#xff09;单位间隔PDF&#xff08;Probability Density Function&#xff09;概率密度函数BER&#xff08;Bit Error Rate&#xff09;误码率TJ&#xff08…

前端八股 CSS 2 选择器

选择器功能&#xff1a;选中特定 DOM节点进行渲染 原始方法 getElementById() getElementByName() 现在方法选择器 分类&#xff1a; id选择器 类选择器 标签选择器 逻辑与选择器 其他类型选择器&#xff1a; 伪类选择器&#xff1a; :link&#xff1a;未被访问的链接…

算法竞赛进阶指南.闇の連鎖

目录 题目算法标签: 树上差分, L C A LCA LCA, 倍增思路代码 题目 352. 闇の連鎖 算法标签: 树上差分, L C A LCA LCA, 倍增 思路 对于一个无向图, 第一次切断树边, 第二次切非树边, 一共多少种方案使得图不连通, 点数和边数都很大, 时间复杂度不能是 O ( n 2 ) O(n ^ 2…

ActiveMQ 与其他 MQ 的对比分析:Kafka/RocketMQ 的选型参考(二)

ActiveMQ、Kafka 和 RocketMQ 详细对比 性能对比 在性能方面&#xff0c;Kafka 和 RocketMQ 通常在高吞吐量场景下表现出色&#xff0c;而 ActiveMQ 则相对较弱。根据相关测试数据表明&#xff0c;Kafka 在处理大规模日志数据时&#xff0c;单机吞吐量可以达到每秒数十万条甚…

Electron 从零开始:构建你的第一个桌面应用

&#x1f5a5;️ Electron 从零开始&#xff1a;构建你的第一个桌面应用 Electron 是一个可以使用 HTML、CSS 和 JavaScript 构建跨平台桌面应用的框架。它将 Chromium 和 Node.js 融合到一个环境中&#xff0c;使 Web 开发者也能轻松开发原生桌面应用。 &#x1f680; 什么是 …