下載的附件名總亂碼?你該去讀一下 RFC 文檔了!

紙上得來終覺淺,絕知此事要躬行html

Web 開發過程當中,相信你們都遇到過附件下載的場景,其中,各瀏覽器下載後的文件名中文亂碼問題或許一度讓你苦惱不已。java

網上搜索一下,大部分都是經過Request Headers中的UserAgent字段來判斷瀏覽器類型,根據不一樣的瀏覽器作不一樣的處理,相似下面的代碼:git

// MicroSoft Browser
if (agent.contains("msie") || agent.contains("trident") || agent.contains("edge")) {
  // filename 特殊處理
}
// firefox
else if (agent.contains("firefox")) {
  // filename 特殊處理
}
// safari
else if (agent.contains("safari")) {
  // filename 特殊處理
}
// Chrome
else if (agent.contains("chrome")) {
  // filename 特殊處理
}
// 其餘
else{
 // filename 特殊處理
}
//最後把特殊處理後的文件名放到head裏
response.setHeader("Content-Disposition",
                    "attachment;fileName=" + filename);

不過,這樣的代碼看起來很魔幻,爲何每一個瀏覽器的處理方式都不同?難道每次新出一個瀏覽器都要作兼容嗎?就沒有一個統一標準來約束一下這幫瀏覽器嗎?github

帶着這個疑惑,我翻閱了 RFC 文檔,最終得出了一個優雅的解決方案:spring

// percentEncodedFileName 爲百分號編碼後的文件名
response.setHeader("Content-disposition",
        "attachment;filename=" + percentEncodedFileName +
        ";filename*=utf-8''" + percentEncodedFileName);

通過測試,這段響應頭能夠兼容市面上全部主流瀏覽器,因爲是 HTTP 協議範疇,因此語言無關。只要按這個規則設置響應頭,就能一勞永逸地解決惱人的附件名中文亂碼問題。chrome

接下來課表明帶你們抽絲剝繭,經過閱讀 RFC 文檔,還原一下這個響應頭的產出過程。瀏覽器

1. Content-Disposition

一切要從 RFC 6266 開始,在這份文檔中,介紹了Content-Disposition響應頭,其實它並不屬於HTTP標準,可是由於使用普遍,因此在該文檔中進行了約束。它的語法格式以下:安全

content-disposition = "Content-Disposition" ":"
                            disposition-type *( ";" disposition-parm )

     disposition-type    = "inline" | "attachment" | disp-ext-type
                         ; case-insensitive
     disp-ext-type       = token

     disposition-parm    = filename-parm | disp-ext-parm

     filename-parm       = "filename" "=" value
                         | "filename*" "=" ext-value

其中的disposition-type有兩種:springboot

  • inline 表明默認處理,通常會在頁面展現
  • attachment 表明應該被保存到本地,須要配合設置filenamefilename*

注意到disposition-parm中的filenamefilename*,文檔規定:這裏的信息能夠用於保存的文件名。app

它倆的區別在於,filename 的 value 不進行編碼,而filename*聽從 RFC 5987中定義的編碼規則:

Producers MUST use either the "UTF-8" ([RFC3629]) or the "ISO-8859-1" ([ISO-8859-1]) character set.

因爲filename*是後來才定義的,許多老的瀏覽器並不支持,因此文檔規定,當兩者同時出如今頭字段中時,須要採用filename*,忽略filename

至此,響應頭的骨架已經呼之欲出了,摘錄 [RFC 6266] 中的示例以下:

Content-Disposition: attachment;
                      filename="EURO rates";
                      filename*=utf-8''%e2%82%ac%20rates

這裏對filename*=utf-8''%e2%82%ac%20rates作一下說明,這個寫法乍一看可能會以爲很奇怪,它實際上是用單引號做爲分隔符,將等號右邊分紅了三部分:第一部分是字符集(utf-8),中間部分是語言(未填寫),最後的%e2%82%ac%20rates表明了實際值。對於這部分的組成,在RFC 2231.section 4 中有詳細說明:

A single quote is used to
   separate the character set, language, and actual value information in
   the parameter value string, and an percent sign is used to flag
   octets encoded in hexadecimal.

2.PercentEncode

PercentEncode 又叫 Percent-encoding 或 URL encoding.

正如前文所述,filename*遵照的是[RFC 5987] 中定義的編碼規則,在[RFC 5987] 3.2中定義了必須支持的字符集:

recipients implementing this specification
MUST support the character sets "ISO-8859-1" and "UTF-8".

而且在[RFC 5987] 3.2.1規定,百分號編碼聽從 RFC 3986.section 2.1中的定義,摘錄以下:

A percent-encoding mechanism is used to represent a data octet in a
component when that octet's corresponding character is outside the
allowed set or is being used as a delimiter of, or within, the
component.  A percent-encoded octet is encoded as a character
triplet, consisting of the percent character "%" followed by the two
hexadecimal digits representing that octet's numeric value.  For
example, "%20" is the percent-encoding for the binary octet
"00100000" (ABNF: %x20), which in US-ASCII corresponds to the space
character (SP).  Section 2.4 describes when percent-encoding and
decoding is applied.

注意了,[RFC 3986] 明確規定了空格 會被百分號編碼爲%20

而在另外一份文檔 RFC 1866.Section 8.2.1 The form-urlencoded Media Type 中卻規定:

The default encoding for all forms is `application/x-www-form-
   urlencoded'. A form data set is represented in this media type as
   follows:

        1. The form field names and values are escaped: space
        characters are replaced by `+', and then reserved characters
        are escaped as per [URL]

這裏要求application/x-www-form-urlencoded類型的消息中,空格要被替換爲+,其餘字符按照[URL]中的定義來轉義,其中的[URL]指向的是RFC 1738 而它的修訂版中和 URL 有關的最新文檔偏偏就是 [RFC 3986]

這也就是爲何不少文檔中描述空格(white space)的百分號編碼結果都是 +%20,如:

w3schools:URL encoding normally replaces a space with a plus (+) sign or with %20.

MDN:Depending on the context, the character ' ' is translated to a '+' (like in the percent-encoding version used in an application/x-www-form-urlencoded message), or in '%20' like on URLs.

那麼問題來了,開發過程當中,對於空格符的百分號編碼咱們應該怎麼處理?

課表明建議你們遵循最新文檔,由於 [RFC 1866] 中定義的狀況僅適用於application/x-www-form-urlencoded類型, 就百分號編碼的定義來講,咱們應該以 [RFC 3986] 爲準,因此,任何須要百分號編碼的地方,都應該將空格符 百分號編碼爲%20,stackoverflow 上也有支持此觀點的答案:When to encode space to plus (+) or %20?

3. 代碼實踐

有了理論基礎,代碼寫起來就水到渠成了,直接上代碼:

@GetMapping("/downloadFile")
public String download(String serverFileName, HttpServletRequest request, HttpServletResponse response) throws IOException {

    request.setCharacterEncoding("utf-8");
    response.setContentType("application/octet-stream");

    String clientFileName = fileService.getClientFileName(serverFileName);
    // 對真實文件名進行百分號編碼
    String percentEncodedFileName = URLEncoder.encode(clientFileName, "utf-8")
            .replaceAll("\\+", "%20");

    // 組裝contentDisposition的值
    StringBuilder contentDispositionValue = new StringBuilder();
    contentDispositionValue.append("attachment; filename=")
            .append(percentEncodedFileName)
            .append(";")
            .append("filename*=")
            .append("utf-8''")
            .append(percentEncodedFileName);
    response.setHeader("Content-disposition",
            contentDispositionValue.toString());
    
    // 將文件流寫到response中
    try (InputStream inputStream = fileService.getInputStream(serverFileName);
         OutputStream outputStream = response.getOutputStream()
    ) {
        IOUtils.copy(inputStream, outputStream);
    }

    return "OK!";
}

代碼很簡單,其中有兩點須要說明一下:

  1. URLEncoder.encode(clientFileName, "utf-8")方法以後,爲何還要.replaceAll("\\+", "%20")

    正如前文所述,咱們已經明確,任何須要百分號編碼的地方,都應該把 空格符編碼爲 %20,而URLEncoder這個類的說明上明確標註其會將空格符轉換爲+:

    The space character "   " is converted into a plus sign "{@code +}".

    其實這並不怪 JDK,由於它的備註裏說明了其遵循的是application/x-www-form-urlencoded( PHP 中也有這麼一個函數,也是這麼個套路)

    Translates a string into {@code application/x-www-form-urlencoded} format using a specific encoding scheme. This method uses the

    因此這裏咱們用.replaceAll("\\+", "%20")+號處理一下,使其徹底符合 [RFC 3986] 的百分號編碼規範。這裏爲了方便說明問題,把全部操做都展示出來了。固然,你徹底能夠本身實現一個PercentEncoder類,豐儉由人。

  2. [RFC 6266] 標準中filename=value是不須要編碼的,這裏的filename=後面的 value 爲何要百分號編碼?

    回顧 [RFC 6266] 文檔, filenamefilename*同時出現時取後者,瀏覽器太老不支持新標準時取前者。

    目前主流的瀏覽器都採用自升級策略,因此大部分都支持新標準------除了老版本IE。老版本的IE對 value 的處理策略是 進行百分號解碼 並使用。因此這裏專門把filename=value進行百分號編碼,用來兼容老版本 IE。

    PS:課表明實測 IE11 及 Edge 已經支持新標準了。

4. 瀏覽器測試

根據下圖 statcounter 統計的 2019 年中國市場瀏覽器佔有率,課表明設計了一個包含中文,英文,空格的文件名 下載-down test .txt用來測試

測試結果:

Browser Version pass
Chrome 84.0.4147.125 true
UC V6.2.4098.3 true
Safari 13.1.2 true
QQ Browser 10.6.1(4208) true
IE 7-11 true
Firefox 79.0 true
Edge 44.18362.449.0 true
360安全瀏覽器12 12.2.1.362.0 true
Edge(chromium) 84.0.522.59 true

根據測試結果可知:基本已經可以兼容市面上全部主流瀏覽器了。

5.總結

回顧本文內容,其實就是瀏覽器兼容性問題引起的附件名亂碼,爲了解決這個問題,查閱了兩類標準文檔:

  1. HTTP 響應頭相關標準

    [RFC 6266]、[RFC 1866]

  2. 編碼標準

    [RFC 5987]、[RFC 2231]、[3986]、[1738]

咱們以 [RFC 6266] 爲切入點,全文總共引用了 6 個 [RFC] 相關文檔,引用都標明瞭出處,感興趣的同窗能夠跟着文章思路閱讀一下原文檔,相信你會對這個問題有更深刻的理解。文中代碼已上傳 github

最後不由要感嘆一下:規範真是個好東西,它就像 Java 語言中的 interface,只制定標準,具體實現留給你們各自發揮。

若是以爲本文對你有幫助,歡迎收藏、分享、在看三連

6.參考資料

[1]RFC 6266: https://tools.ietf.org/html/rfc6266

[2]RFC 5987: https://tools.ietf.org/html/rfc5987

[3]RFC 2231: https://tools.ietf.org/html/rfc2231

[4]RFC 3986: https://tools.ietf.org/html/rfc3986

[5]RFC 1866: https://tools.ietf.org/html/rfc1866

[6]RFC 1738: https://tools.ietf.org/html/rfc1738

[7]When to encode space to plus (+) or %20?: https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20


👇關注 Java課表明,獲取最新 Java 乾貨👇

相關文章
相關標籤/搜索