每個(gè) Web 開(kāi)發(fā)者都應(yīng)該知道的關(guān)于 URL 編碼的知識(shí)
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
本文首先闡述了人們關(guān)于統(tǒng)一資源定位符(URL)編碼的普遍的誤讀,其后通過(guò)闡明HTTP場(chǎng)景下的URL encoding 來(lái)引出我們經(jīng)常遇到的問(wèn)題及其解決方案。本文并不特定于某類(lèi)編程語(yǔ)言,我們?cè)贘ava環(huán)境下闡釋問(wèn)題,最后從Web應(yīng)用的多個(gè)層次描述如何解決URL編碼的問(wèn)題來(lái)結(jié)尾。 目錄
簡(jiǎn)介當(dāng)我們每天上網(wǎng)沖浪時(shí),有一些技術(shù)我們無(wú)時(shí)無(wú)刻不在面對(duì)。有數(shù)據(jù)本身(網(wǎng)頁(yè)),數(shù)據(jù)的格式化,能夠讓我們獲取數(shù)據(jù)的傳輸機(jī)制,以及讓W(xué)eb網(wǎng)絡(luò)能夠真正成為Web的基礎(chǔ)及根本:從一頁(yè)到另一頁(yè)的鏈接。這些鏈接都是URL。 通用URL語(yǔ)法我敢說(shuō)每個(gè)人在其一生中至少見(jiàn)過(guò)一次URL。比如"http://www.google.com",就是一個(gè)URL。一個(gè)URL是一個(gè)統(tǒng)一資源定位器 ,事實(shí)上它指向了一個(gè)網(wǎng)頁(yè)(大多數(shù)情況下)。實(shí)際上,自從1994年的第一版規(guī)范開(kāi)始,URL就有了一個(gè)良好定義的結(jié)構(gòu)。 我們能從"http://www.google.com" 這個(gè)URL中讀出下列詳細(xì)信息:
如果我們看一個(gè)更復(fù)雜的URL,比如 "https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third" 我們就能獲取到下列信息:
協(xié)議 (即scheme,如上面的http和https (安全HTTP)) 定義了URL中其余部分的結(jié)構(gòu)。大多數(shù)互聯(lián)網(wǎng)URL協(xié)議擁有通用的開(kāi)頭,包括用戶(hù),密碼,主機(jī)名和端口,后面才是每個(gè)協(xié)議具體的部分。這個(gè)通用的部分負(fù)責(zé)處理認(rèn)證,同時(shí)它也有能力知道為了請(qǐng)求數(shù)據(jù)應(yīng)該鏈接到哪兒。 HTTP URL語(yǔ)法對(duì)于HTTP URL (使用http 或 https 協(xié)議),URL的scheme描述部分定義了數(shù)據(jù)的路徑(path),后面是可選的query 和 fragment。 path 部分看上去是一個(gè)分層的結(jié)構(gòu),類(lèi)似于文件系統(tǒng)中文件夾和文件的分層結(jié)構(gòu)。path由"/"字符開(kāi)始,每一個(gè)文件夾由"/"分隔,最后是文件。例如"/photos/egypt/cairo/first.jpg"有四個(gè)路徑片段(segment):"photos"、"egypt"、"cairo" 和 "first.jpg",可以由此推出:"first.jpg" 文件在文件夾"cairo"中,而"egypt" 文件夾位于web站點(diǎn)的根文件夾"photos"里面。 每一個(gè)path片段 可以有可選的 path參數(shù) (也叫 matrix參數(shù)),這是在path片段的最后由";"開(kāi)始的一些字符。每個(gè)參數(shù)名和值由"="字符分隔,像這樣:"/file;p=1",這定義了path片段 "file"有一個(gè) path參數(shù) "p",其值為"1"。這些參數(shù)并不常用 — 這得清楚 — 但是它們確實(shí)是存在,而且從 Yahoo RESTful API 文檔我們能找到很好的理由去使用它們:
在 路徑(path)部分之后是 查詢(xún) (query)部分,它和 路徑 之間由一個(gè)“?”隔開(kāi), 查詢(xún)部分包含了一個(gè)由“&”分隔開(kāi)的參數(shù)列表,每一個(gè)參數(shù)由參數(shù)名稱(chēng)、“=”號(hào)以及參數(shù)值組成。比如"/file?q=2"定義了一個(gè) 查詢(xún)參數(shù) "q" ,它的值是"2"。這在提交 HTML表單時(shí),或者當(dāng)你使用諸如Google搜索等應(yīng)用時(shí), 用的非常多。 一個(gè)HTTP URL的最后部分是一個(gè)段落(fragment)部分,用來(lái)指向HTML文件中具體的某個(gè)部分,而不是整個(gè)HTML頁(yè)面。比如說(shuō),當(dāng)你點(diǎn)擊鏈接時(shí)瀏覽器自動(dòng)滾屏到某個(gè)部分而不是從頁(yè)面最頂部開(kāi)始展示,就說(shuō)明你點(diǎn)擊了一個(gè)擁有段落部分的URL。 URL 語(yǔ)法http URL 方案最初由 RFC 1738 定義(實(shí)際上,在之前的 RFC 1630也有涉及),而在 http URL 方案被重新定義之前,整個(gè) URL 語(yǔ)法就已經(jīng)由擴(kuò)展了幾次 以適應(yīng)發(fā)展的規(guī)范進(jìn)化為一套 統(tǒng)一資源標(biāo)識(shí)符(Uniform Resource Identifiers 即 URIs)。 對(duì)于 URLs 如何拼裝,各部分如何分隔有一套語(yǔ)法。例如:"://"分隔方案和主機(jī)部分。主機(jī)同路徑片段部分由"/"分隔,而查詢(xún)部分緊跟在"?"之后。這意味著有些字符為語(yǔ)法保留。有些為整個(gè)URIs保留,而有些則被特定方案保留。所有出現(xiàn)在不應(yīng)出現(xiàn)位置的 保留符(例如路徑片段——以文件名為例——可能包含"?")必須被URL 編碼。 URL 編碼將字符轉(zhuǎn)變成對(duì) URL 解析無(wú)意義的無(wú)害形式。它將字符轉(zhuǎn)化成為一種特定字符編碼的字節(jié)序列,然后將字節(jié)轉(zhuǎn)換為16進(jìn)制形式,并將其前面加上"%"。問(wèn)號(hào)的 URL 編碼形式為"%3F"。 我們可以將指向 "to_be_or_not_to_be?.jpg"圖片的 URL 寫(xiě)成:"http://example.com/to_be_or_not_to_be%3F.jpg",這樣就沒(méi)有人會(huì)認(rèn)為這兒可能由一個(gè)查詢(xún)部分了。 現(xiàn)今多數(shù)瀏覽器顯示 URLs 前都會(huì)對(duì)其解碼(將百分號(hào)編碼字節(jié)轉(zhuǎn)回其原本字符),并在獲取其網(wǎng)絡(luò)資源的時(shí)候重新編碼。這樣一來(lái),很多用戶(hù)從未意識(shí)到編碼的存在。 另一方面,網(wǎng)頁(yè)作者,開(kāi)發(fā)者必須明確認(rèn)識(shí)到這一點(diǎn),因?yàn)檫@里存在著很多陷阱。 URL常見(jiàn)陷阱如果你正和URL打交道,了解下能夠避免的常見(jiàn)陷阱絕對(duì)是值得的?,F(xiàn)在我們給大家介紹下不僅限于此的一些常見(jiàn)陷阱。 使用哪類(lèi)字符編碼?URL編碼規(guī)范并沒(méi)有定義使用何種字符編碼形式去編碼字節(jié)。一般的ASCII字母數(shù)字字符并不需要轉(zhuǎn)義,但是ASCII之外的保留字需要(例如法語(yǔ)單詞“nœud”中的"œ")。我們必須提出疑問(wèn),應(yīng)該使用哪類(lèi)字符編碼來(lái)編碼URL字節(jié)。 當(dāng)然如果只有Unicode的話(huà),這個(gè)世界就會(huì)清凈很多。因?yàn)槊總€(gè)字符都包含其中,但是它只是一個(gè)集合,或者說(shuō)是列表如果你愿意,它本身并不是一中編碼。Unicode可以使用多種方式進(jìn)行編碼,譬如UTF-8或者UTF-16(也有其它格式),但是問(wèn)題并沒(méi)有解決:我們應(yīng)該使用哪類(lèi)字符來(lái)編碼URL(通常也指URI)。 標(biāo)準(zhǔn)并沒(méi)有定義一個(gè)URI應(yīng)該以何種方式指定其編碼,所以其必須從環(huán)境信息中進(jìn)行推導(dǎo)。對(duì)于HTTP URL,它可以是HTML頁(yè)面的編碼格式,或HTTP頭的。這通常會(huì)讓人迷惑,也是許多錯(cuò)誤的根源。事實(shí)上,最新版的URI標(biāo)準(zhǔn) 定義了新的URI scheme將采用UTF-8,host(甚至已有的scheme)也使用UTF-8,這讓我更加懷疑:難道host和path真的可以使用不同的編碼方式? 每一部分的保留字都是不同。是的,他們是,是的,他們,是的,他們是。。。 對(duì)于一個(gè)httpd連接,路徑片段部分中的空格被編碼為"%20"(不,完全沒(méi)有"+"),而“+”字符在路徑片段部分可以保持不編碼。 現(xiàn)在,在查詢(xún)部分,一個(gè)空格可能會(huì)被編碼為“+”(為了向后兼容:不要試圖在URI標(biāo)準(zhǔn)去搜索他)或者“%20”,當(dāng)作為“+”字符(作為個(gè)統(tǒng)配符的結(jié)果)會(huì)被編譯為“%2B”。 這意味著“blue+light blue”字串,如果在路徑部分或者查詢(xún)部分,將會(huì)有不同的編碼。比如得到"http://example.com/blue+light%20blue?blue%2Blight+blue"這樣的編碼形式,這樣我們不需從語(yǔ)法上分析url結(jié)構(gòu),就可以推導(dǎo)這個(gè)url的整個(gè)結(jié)構(gòu)是可能 考慮如下組裝URL的Java代碼片段 編碼URL并不是為了轉(zhuǎn)義保留字而進(jìn)行的簡(jiǎn)單字符迭代,我們需要確切的知道哪個(gè)URL部份有哪些保留字,而有針對(duì)性的進(jìn)行編碼。 這也意味著URL重寫(xiě)過(guò)濾器如果不考慮合適的編碼細(xì)節(jié)而對(duì)URL直接進(jìn)行分段轉(zhuǎn)換通常是有問(wèn)題的。對(duì)URL進(jìn)行編碼而不考慮具體的分段規(guī)則是不切實(shí)際的。 保留字不是你想象的那樣大多數(shù)人不知道"+"在路徑部分是被允許的并且特指正號(hào)而不是空格。其他類(lèi)似的有:
這樣下面的地址雖然看起來(lái)有點(diǎn)混亂:"http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'()*+,;==#/?:@-._~!$&'()*+,;=" 按照上面的規(guī)則,其實(shí)上是一個(gè)合法的地址。 不用奇怪,上面路徑可以被解析為:
不能分析解碼后的URLURL的語(yǔ)法只在它被解碼前是有意義的,一旦解碼就可能出現(xiàn)保留字。 例如"http://example.com/blue%2Fred%3Fand+green" 在解碼前由如下部分組成:
這樣看來(lái), 我們是在請(qǐng)求一個(gè)名為"blue/red?and+green"的文件,而不是一個(gè)位于"blue"文件夾下的名為"red?and+green"的文件。 如果我們把它解碼為"http://example.com/blue/red?and+green",我們將得到如下部分:
這明顯是錯(cuò)誤的,所以,對(duì)保留字和URL各部分的分析必須在URL解碼之前完成。這意味著URL重寫(xiě)過(guò)濾器不應(yīng)當(dāng)在嘗試匹配之前解碼URL,當(dāng)且僅當(dāng)保留字允許進(jìn)行URL編碼時(shí)才可以(有時(shí)符合這種情形,有時(shí)不符合,這取決于你的應(yīng)用)。 解碼后的URL不能被再編碼為同樣的形式如果你解碼"http://example.com/blue%2Fred%3Fand+green" 為"http://example.com/blue/red?and+green",然后對(duì)它進(jìn)行編碼(哪怕使用一個(gè)對(duì)URL每一部分都很了解的編碼器),你將會(huì)得到"http://example.com/blue/red?and+green",這是因?yàn)樗呀?jīng)是一個(gè)有效的URL。它跟我們解碼之前的URL非常的不同。 用Java正確處理URL當(dāng)你覺(jué)得自己已經(jīng)拿到了URL的黑腰帶(柔道中的最高級(jí)別--譯者注),你將會(huì)發(fā)現(xiàn)仍有一些Java里特有的、URL相關(guān)的陷阱。如果沒(méi)有一個(gè)強(qiáng)大的心臟,你很難正確的處理URL。 不要用java.net.URLEncoder或者java.net.URLDecoder來(lái)處理整個(gè)URL不開(kāi)玩笑。這些類(lèi)不是用來(lái)編碼或解碼URL的,API文檔中清楚的寫(xiě)著:
這不是給URL用的。充其量它類(lèi)似于查詢(xún) 部分的編碼方式。使用它來(lái)編碼或解碼整個(gè)URL是錯(cuò)誤的。你肯定以為標(biāo)準(zhǔn)的JDK一定會(huì)有一個(gè)標(biāo)準(zhǔn)的類(lèi)來(lái)正確的處理URL編碼(是這樣,只不過(guò)是各部分分開(kāi)處理的),但是要么是壓根沒(méi)有,要么是我們還沒(méi)有發(fā)現(xiàn)。不過(guò),這種臆測(cè)導(dǎo)致許多人錯(cuò)用了URLEncoder。 在對(duì)每一部分編碼之前不要拼裝URL正如我們已經(jīng)講過(guò)的:完整構(gòu)建后的URL不能再被編碼。 以下面的代碼為例: 如果"a/b?c" 是一個(gè)路徑片段,那么不可能把"http://example.com/a/b?c" 轉(zhuǎn)換回之前它的原樣,因?yàn)樗銮墒且粋€(gè)有效的URL。之前我們已經(jīng)解釋過(guò)這一點(diǎn)。 下面是正確的代碼: 這里我們使用了一個(gè)工具類(lèi)URLUtils,它是我們自己開(kāi)發(fā)的,因?yàn)榫W(wǎng)絡(luò)上找不到一個(gè)詳盡的足夠快的工具類(lèi)。上面的代碼會(huì)帶給你正確編碼的URL "http://example.com/a%2Fb%3Fc"。 注意,同樣的方式也適用于查詢(xún)子串: 這會(huì)給你"http://example.com/?query=a&b==c",這是個(gè)有效的URL,而不是我們想得到的"http://example.com/?query=a%26b==c"。 不要期望 URI.getPath() 給你結(jié)構(gòu)化的數(shù)據(jù)因?yàn)橐坏┮粋€(gè)URL被解碼,句法信息就會(huì)丟失,下面這樣的代碼就是錯(cuò)誤的:
它會(huì)先將路徑 "a%2Fb%3Fc"解碼為 "a/b?c",然后在不應(yīng)該分割的地方將地址分割為地址片段。 正確的代碼使用的是 未解碼的路徑 :
注意路徑參數(shù)仍然存在:如果需要的話(huà)再處理它們。 不要期望 Apache Commons HTTPClient的URI類(lèi)能夠正確的做對(duì)Apache Commons HTTPClient 3的 URI 類(lèi)使用了Apache Commons Codec的URLCodec來(lái)做 URL編碼, 正如 API文檔提到的 它是有問(wèn)題的,因?yàn)樗噶撕褪褂胘ava.net.URLEncoder同樣的錯(cuò)誤。它不但使用了錯(cuò)誤的編碼器,還錯(cuò)誤的 按照每一部分都具有同樣的預(yù)定設(shè)置進(jìn)行解碼。 在web應(yīng)用的每一層修復(fù)URL編碼問(wèn)題近來(lái)我們已經(jīng)被動(dòng)修復(fù)了許多應(yīng)用中的URL編碼問(wèn)題。從在Java中支持它,到低層次的URL重寫(xiě)。這里我們會(huì)列出一些必要的修改。 總是在創(chuàng)建的時(shí)候進(jìn)行URL編碼在我們的 HTML文件中,我們將所有出現(xiàn):
的地方替換為:
查詢(xún)參數(shù)也是類(lèi)似的。 確保你的URL-rewrite過(guò)濾器正確的處理網(wǎng)址Url 重寫(xiě)過(guò)濾器是一個(gè)重寫(xiě)過(guò)濾器,我們?cè)趕eam中用于轉(zhuǎn)化漂亮的地址去應(yīng)用依賴(lài)的網(wǎng)址。 例如,我們用它把http://beta.visiblelogistics.com/view/resource/FOO/bar轉(zhuǎn)化為http://beta.visiblelogistics.com/resources/details.seam?owner=FOO&name=bar。 很明顯,這個(gè)過(guò)程包含了一些字符串從一個(gè)地址到另一個(gè)地址,這意味著我們要從路徑部分解碼并且把它重新編碼為另一個(gè)查詢(xún)值部分。 我們起初的規(guī)則,如下所示:
從這我們可以看到在重寫(xiě)過(guò)濾器中只有兩種方法處理網(wǎng)址重寫(xiě):每一個(gè)的網(wǎng)址先被解碼去做規(guī)則匹配(<to>模式),或者它不可用,所有規(guī)則去處理解碼。在我們看來(lái)后者是比較好的選擇,特別是當(dāng)你要移動(dòng)網(wǎng)址部分周?chē)?,或者想去包含URL解碼路徑分隔符的匹配路徑部分時(shí)候。 在替換模式中(<to>模式)你可以使用內(nèi)建的函數(shù)escape(String)和unescape(String)處理網(wǎng)站轉(zhuǎn)碼和解碼。 在撰寫(xiě)這個(gè)文章的時(shí)候,Url Rewrite Filter Beta 3.2有一些bugs,限制住我們提高URL-correctness:
We therefore made a big patch fixing a few issues like URL decoding, and adding the inline functionsescapePathSegment(String)andunescapePathSegment(String). 我們因此做了一個(gè)大修正補(bǔ)丁,用于修正諸如網(wǎng)址解碼問(wèn)題以及增加內(nèi)建函建escapePathSegment(String) 和 unescapePathSegment(String) 我們現(xiàn)在可以這樣寫(xiě),幾乎不會(huì)有錯(cuò)誤
唯一可能出問(wèn)題的地方是由于我們的補(bǔ)丁還不能解決以下的問(wèn)題:
我們一有時(shí)間,我們就會(huì)發(fā)布第二個(gè)補(bǔ)丁。 正確使用Apache mod-rewriteApache mod-rewrite是一個(gè)Apache Web服務(wù)器的網(wǎng)址重寫(xiě)模塊。例如用它來(lái)把 http://beta.visiblelogistics.com/foo 的流量代理到http://our-internal-server:8080/vl/foo。 這是最后的要修正的事情,就像是Url Rewrite Filter,他默認(rèn)解碼網(wǎng)址給我們,并且從新編碼重寫(xiě)過(guò)得網(wǎng)址給我們,這其實(shí)上是錯(cuò)誤的,因?yàn)?解碼的網(wǎng)址不能被重新編碼"。 有一種方法可以避免這種行為,至少在我們的案例中我們沒(méi)有轉(zhuǎn)化一個(gè)網(wǎng)址部分到另一個(gè)網(wǎng)址,例如,我們不需要解碼一個(gè)路徑部分并且重新編碼它到一個(gè)查詢(xún)部分:沒(méi)有加碼也沒(méi)有重編碼。 我們通過(guò)THE_REQUEST來(lái)網(wǎng)址匹配來(lái)完成工作。他是完全的HTTP請(qǐng)求(包括HTTP方法和版本)聯(lián)合解碼。我們只要取host后面的URL部分,改變host和預(yù)設(shè)的/v/前綴和tada ... 結(jié)論我希望闡明一些URL技巧和常見(jiàn)的錯(cuò)誤。簡(jiǎn)而言之,能把它說(shuō)明白就夠了,但這不是一些人想象的那樣簡(jiǎn)單的。我們展示了java常見(jiàn)的錯(cuò)誤和一個(gè)web 應(yīng)用部署的整個(gè)過(guò)程?,F(xiàn)在每個(gè)讀者都應(yīng)該是一個(gè)URL專(zhuān)家了,并且我們希望不要在看見(jiàn)相關(guān)bugs再出現(xiàn)。請(qǐng)求SUN公司,請(qǐng)為URL encoding/decoding逐項(xiàng)的增加標(biāo)準(zhǔn)支持。 該文章在 2013/7/5 12:42:16 編輯過(guò) |
關(guān)鍵字查詢(xún)
相關(guān)文章
正在查詢(xún)... |