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.pswp.cn/diannao/81738.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81738.shtml
英文地址,請注明出處:http://en.pswp.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; 什么是 …