Web 應用提供下載功能時,若文件名包含中文,不同瀏覽器對 Content-Disposition 標頭中 filename 參數的編碼處理不一致,導致下載對話框顯示亂碼。典型差異:
filename 為 UTF-8 URL 編碼(URLEncoder.encode)。filename*=UTF-8''encoded),也兼容 ISO-8859-1 編碼的 filename。本文提供一個零依賴、可直接集成的工具類,根據 User-Agent 自動選擇編碼策略,兼容主流瀏覽器。
package com.example.util;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 下載文件名編碼工具類
* 根據 User-Agent 自動選擇編碼方式,解決中文文件名在不同瀏覽器下的亂碼問題。
*/
public class DownloadFilenameEncoder {
/**
* 編碼下載文件名
*
* @param request HttpServletRequest,用於獲取 User-Agent
* @param fileName 原始文件名(含中文)
* @return 編碼後的文件名,可直接用於 Content-Disposition 的 filename 參數
*/
public static String encode(HttpServletRequest request, String fileName) {
if (fileName == null) return "";
String userAgent = request.getHeader("User-Agent");
try {
// IE / 老版 Edge (Trident 引擎)
if (userAgent != null && userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// URLEncoder.encode 會將空格編碼為 +,需替換為 %20 符合 RFC
return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
}
// 現代瀏覽器:Firefox, Chrome, Safari, 新版 Edge (Chromium)
// 採用 ISO-8859-1 "偽編碼",瀏覽器會按 UTF-8 解碼
return new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
// 理論不會發生,JDK 必備編碼
return fileName;
}
}
/**
* 生成完整的 Content-Disposition 標頭值(推薦使用)
* 同時輸出 filename (兼容舊瀏覽器) 和 filename* (RFC 5987 標準),
* 瀏覽器會優先使用支持的那一個。
*
* @param request HttpServletRequest
* @param fileName 原始文件名
* @return 完整標頭值,如:attachment; filename="fallback.txt"; filename*=UTF-8''real%20name.txt
*/
public static String buildContentDisposition(HttpServletRequest request, String fileName) {
String encodedLegacy = encode(request, fileName);
String encodedRfc5987;
try {
encodedRfc5987 = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
encodedRfc5987 = encodedLegacy;
}
return "attachment; filename=\"" + encodedLegacy + "\"; filename*=UTF-8''" + encodedRfc5987;
}
}
@GetMapping("/download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
String originalName = "產品手冊_2024版.pdf";
String contentDisposition = DownloadFilenameEncoder.buildContentDisposition(request, originalName);
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", contentDisposition);
// response.setContentLengthLong(file.length()); // 建議設置
try (InputStream in = new FileInputStream("/data/files/manual.pdf");
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
}
}
| 瀏覽器類型 | User-Agent 關鍵字 | 編碼方式 | 標頭示例 |
|---|---|---|---|
| IE 6-11 | MSIE / Trident | UTF-8 URL Encode | filename="%E4%B8%AD%E6%96%87.txt" |
| Firefox | Firefox | ISO-8859-1 偽編碼 / RFC 5987 | filename="中文.txt"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt |
| Chrome / 新版 Edge | Chrome / Edg | ISO-8859-1 偽編碼 / RFC 5987 | 同 Firefox |
| Safari | Safari | ISO-8859-1 偽編碼 / RFC 5987 | 同 Firefox |
buildContentDisposition() 同時輸出 filename 和 filename*,現代瀏覽器優先使用 RFC 5987 格式的 filename*,舊瀏覽器回退到 filename,實現最廣兼容。URLEncoder.encode 將空格編碼為 +,但 RFC 3986 規定路徑中空格應為 %20,故需 .replaceAll("\\+", "%20")。\、/、:、*、?、"、<、>、| 等非法字符,防止路徑遍歷或標頭注入。ServerHttpResponse 設置標頭。filename 長度有 255 字節限制,超長文件名需截斷或僅保留關鍵部分。Content-Disposition,需在網絡層面排查。通過 User-Agent 判斷瀏覽器類型,採用「舊瀏覽器 URL 編碼 + 現代瀏覽器 ISO-8859-1 偽編碼」雙策略,並建議同時輸出 RFC 5987 標準的 filename* 參數,可徹底解決中文下載文件名在主流瀏覽器中的亂碼問題。工具類無第三方依賴,複製即用,適合所有 Servlet 容器環境。