Java如何正確處理下載文件時(shí)HTTP頭的編碼問(wèn)題
通常HTTP消息包括客戶機(jī)向服務(wù)器的請(qǐng)求消息和服務(wù)器向客戶機(jī)的響應(yīng)消息,今天來(lái)講解下正確處理下載文件時(shí)HTTP頭的編碼問(wèn)題,需要的朋友可以參考下
最近在做項(xiàng)目時(shí)遇到了一個(gè) case :需要實(shí)現(xiàn)一個(gè)強(qiáng)制下載功能(即強(qiáng)制彈出下載對(duì)話框),并且文件名必須保持和用戶之前上傳時(shí)相同(可能包含非 ASCII 字符)。
前一個(gè)需求很容易實(shí)現(xiàn):使用 HTTPHeader的Content-Disposition: attachment
即可,還可以配合Content-Type: application/octet-stream
來(lái)確保萬(wàn)無(wú)一失。而后一個(gè)需求就比較蛋疼了,牽扯到 Header 的編碼問(wèn)題(文件名是作為 filename 參數(shù)放在 Content-Disposition 里面的)。眾所周知, HTTP Header 中的 Content-Type 可以指定內(nèi)容的編碼,可 Header 本身的編碼又該如何制定?甚至, Header 究竟是否允許非 ASCII 編碼呢?
如果放任編碼問(wèn)題不管,那么恭喜你,你一定會(huì)遇到在某個(gè)系統(tǒng)及瀏覽器下下載文件時(shí)文件名亂碼的情況。如果你嘗試搜索解決,那么再一次恭喜你,你會(huì)找到一堆自相矛盾的解決方案(我可以負(fù)責(zé)任地告訴你,其中的99%都是不符合標(biāo)準(zhǔn)的 trick 罷了)。讓我們來(lái)看看到底應(yīng)該如何優(yōu)雅完美地解決這個(gè)問(wèn)題吧!
為了探索這個(gè)問(wèn)題,我走了不少?gòu)澛?。從自己嘗試,到 Google 、百度(分別嘗試過(guò)中英文搜索),再到閱讀 Discuz 等經(jīng)典項(xiàng)目的源碼,眾說(shuō)紛紜、莫衷一是。最后我才想到回歸 RFC ,從標(biāo)準(zhǔn)文檔中找辦法,果然有所收獲。由于探究過(guò)程實(shí)在太曲折,我就先把標(biāo)準(zhǔn)做法寫下來(lái)。
應(yīng)該這樣設(shè)置 Content-Disposition :
Content-Disposition: attachment; filename="$encoded_fname"; filename*=utf-8''$encoded_fname
其中,$encoded_fname指的是將 UTF-8 編碼的原始文件名按照RFC 3986進(jìn)行百分號(hào) urlencode 后得到的( PHP 中使用
rawurlencode()
函數(shù))。這幾行也可以合并為一行,推薦使用一個(gè)空格隔開(kāi)。另外,為了兼容 IE6 ,請(qǐng)保證原始文件名必須包含英文擴(kuò)展名!
好了,接下來(lái)我們來(lái)看看為什么要這么做以及為什么能這么做。
首先,根據(jù) HTTP 1.1 協(xié)議規(guī)范(RFC 2616 Section 4), HTTP 消息格式其實(shí)是基于古老的 ARPA INTERNET TEXT MESSAGES (RFC 822 Section 3),根據(jù)其規(guī)定,消息只能是 ASCII 編碼的。RFC 2616 Section 2.2又一次強(qiáng)調(diào), TEXT 中若要使用其他字符集,必須使用RFC 2047的規(guī)則將字符串編碼為 ASCII 碼(事實(shí)上這個(gè)規(guī)則原本是針對(duì) MIME 的擴(kuò)展,使用的是 base64 編碼,格式與百分號(hào)編碼有很大不同)??偠灾凑諛?biāo)準(zhǔn), HTTP Header 中的文本數(shù)據(jù)必須是 ASCII 編碼的。
filename="TEXT"
;這是 RFC 2616 標(biāo)準(zhǔn),TEXT必須是 ASCII 字符且被認(rèn)為就是“原文”
filename*=charset'lang'encoded-text
;這是按照 RFC 2047 擴(kuò)展后的,注意格式上的細(xì)微區(qū)別,采用 base64 編碼(編碼結(jié)果也是 ASCII 字符)
然而,事實(shí)上在1999年 HTTP 1.1 標(biāo)準(zhǔn)推出之時(shí), Content-Dispostion 這個(gè) Header 尚不是正式標(biāo)準(zhǔn)的一部分,只不過(guò)是因?yàn)楸粡V泛使用而從 MIME 標(biāo)準(zhǔn)中直接借用過(guò)來(lái)了而已(RFC 2616 Section 19.5.1)。因而幾乎沒(méi)有瀏覽器去支持 Content-Disposition 的多語(yǔ)言編碼特性這樣一個(gè)“擴(kuò)展特性的擴(kuò)展特性”(事實(shí)上, HTTP 1.1 草案中建議的使用 RFC 2047 來(lái)進(jìn)行多語(yǔ)言編碼的特性從未被主流瀏覽器支持過(guò))。
可是這個(gè)問(wèn)題卻的確是現(xiàn)實(shí)需要的,所以瀏覽器就各自想出了一些辦法:
- IE支持兩種格式的混合版:
filename="encoded_text"
(這里采用的是百分號(hào)編碼)。本來(lái)按照 RFC 2616 ,引號(hào)內(nèi)的部分應(yīng)當(dāng)直接被當(dāng)作內(nèi)容,就算它“看起來(lái)像是編碼后的字符串”;可是IE卻會(huì)“自動(dòng)”對(duì)這樣的文件名進(jìn)行解碼——前提是該文件名必須有一個(gè)不會(huì)被編碼的后綴名(即正常的英文字母后綴名)! - 其他一些瀏覽器則支持一種更為粗暴的方式——允許在
filename="TEXT"
中直接使用 UTF-8 編碼的字符串!
這兩類瀏覽器的行為是彼此互不兼容的。所以你可以判斷 UA 然后對(duì)IE使用前一種辦法,其他瀏覽器使用后一種,這樣便可以達(dá)到一般情況下能夠 just work 的效果( Discuz 就是這么做的)。不過(guò)對(duì)于 Opera 和 Safari ,這樣做可能不一定有效。
時(shí)代在進(jìn)步,2010年RFC 5987發(fā)布,正式規(guī)定了 HTTP Header 中多語(yǔ)言編碼的處理方式,應(yīng)當(dāng)采用類似 MIME 擴(kuò)展的parameter*=charset'lang'value
的格式,但是其中 value 應(yīng)根據(jù)RFC 3986 Section 2.1使用百分號(hào)進(jìn)行編碼,并且規(guī)定瀏覽器至少應(yīng)該支持 ASCII 和 UTF-8 。隨后,2011年RFC 6266發(fā)布,正式將 Content-Disposition 納入 HTTP 標(biāo)準(zhǔn),并再次強(qiáng)調(diào)了 RFC 5987 中多語(yǔ)言編碼的方法,還給出了一個(gè)范例用于解決向后兼容的問(wèn)題——就是我在一開(kāi)始給出的例子:
Content-Disposition: attachment; filename="encoded_text"; filename*=utf-8''encoded_text
在這個(gè)例子中,對(duì)于較新的 Firefox 、 Chrome 、 Opera 、 Safari 等瀏覽器,都支持新標(biāo)準(zhǔn)規(guī)定的 filename* ,并且會(huì)優(yōu)先使用,所以盡管 filename=”encoded_text” 不被它們支持,仍然不會(huì)有問(wèn)題;至于使用 UTF-8 只是因?yàn)樗菢?biāo)準(zhǔn)中強(qiáng)制要求必須支持的。而對(duì)于舊版本的IE瀏覽器,它們無(wú)法識(shí)別后面的 filename* ,會(huì)自動(dòng)忽略并使用舊的 filename 。這樣一來(lái)就完美解決了多瀏覽器的多語(yǔ)言兼容問(wèn)題,既不需要 UA 判斷,也符合標(biāo)準(zhǔn)。
P.S.為什么 PHP 要使用rawurlencode()
函數(shù)呢?因?yàn)檫@才是真正符合 RFC 3986 的“百分號(hào)URL編碼”,只是由于歷史原因,之前先有了一個(gè)urlencode()
函數(shù)用于實(shí)現(xiàn) HTTP POST 中的類似的編碼規(guī)則,故而只好用這么一個(gè)奇怪的名字。兩者的區(qū)別在于前者會(huì)把空格編碼為%20,而后者則會(huì)編碼為+號(hào)。如果使用后者,那么IE6在下載帶有空格的文件名時(shí)空格會(huì)變?yōu)榧犹?hào)。一般情況下,你是不會(huì)用到urlencode()
這個(gè)函數(shù)的( Discuz 某些版本中錯(cuò)誤地使用它來(lái)進(jìn)行文件名編碼,從而導(dǎo)致空格變加號(hào)的BUG)。
到此這篇關(guān)于Java如何正確處理下載文件時(shí)HTTP頭的編碼問(wèn)題的文章就介紹到這了,更多相關(guān)Java下載文件HTTP頭編碼問(wèn)題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
OpenJDK源碼解析之System.out.println詳解
這篇文章主要介紹了OpenJDK源碼解析之System.out.println詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04通過(guò)weblogic API解析如何獲取weblogic中服務(wù)的IP和端口操作
這篇文章主要介紹了通過(guò)weblogic API解析如何獲取weblogic中服務(wù)的IP和端口操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Spring @Profile注解實(shí)現(xiàn)多環(huán)境配置
這篇文章主要介紹了Spring @Profile注解實(shí)現(xiàn)多環(huán)境配置,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04解決springboot啟動(dòng)失敗的問(wèn)題('hibernate.dialect'?not?set)
這篇文章主要介紹了解決springboot啟動(dòng)失敗的問(wèn)題('hibernate.dialect'?not?set),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12華為鴻蒙系統(tǒng)應(yīng)用開(kāi)發(fā)工具 DevEco Studio的安裝和使用圖文教程
HUAWEI DevEco Studio 是華為消費(fèi)者業(yè)務(wù)為開(kāi)發(fā)者提供的集成開(kāi)發(fā)環(huán)境(IDE),旨在幫助開(kāi)發(fā)者快捷、方便、高效地使用華為EMUI開(kāi)放能力。這篇文章主要介紹了華為鴻蒙系統(tǒng)應(yīng)用開(kāi)發(fā)工具 DevEco Studio的安裝和使用圖文教程,需要的朋友可以參考下2021-04-04Mybatis結(jié)果生成鍵值對(duì)的實(shí)例代碼
這篇文章主要介紹了Mybatis結(jié)果生成鍵值對(duì)的實(shí)例代碼,以及MyBatis返回Map鍵值對(duì)數(shù)據(jù)的實(shí)現(xiàn)方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的的朋友參考下2017-02-02Java中的常用時(shí)間日期類總結(jié)(Date、DateFormat)
在Java開(kāi)發(fā)中處理時(shí)間和日期是相當(dāng)常見(jiàn)的任務(wù),無(wú)論是計(jì)算日期差異、格式化日期顯示、解析日期字符串還是進(jìn)行日期計(jì)算,都需要一些時(shí)間和日期處理的技巧,這篇文章主要給大家介紹了關(guān)于Java中常用時(shí)間日期類(Date、DateFormat)的相關(guān)資料,需要的朋友可以參考下2024-08-08