本文首先闡述了人們關(guān)于統(tǒng)一資源定位符(URL)編碼的普遍的誤讀,其后通過闡明HTTP場景下的URLencoding來引出我們經(jīng)常遇到的問題及其解決方案。本文并不特定于某類編程語言,我們在Java環(huán)境下闡釋問題,最后從Web應(yīng)用的多個層次描述如何解決URL編碼的問題來結(jié)尾。
簡介
當我們每天上網(wǎng)沖浪時,有一些技術(shù)我們無時無刻不在面對。有數(shù)據(jù)本身(網(wǎng)頁),數(shù)據(jù)的格式化,能夠讓我們獲取數(shù)據(jù)的傳輸機制,以及讓W(xué)eb網(wǎng)絡(luò)能夠真正成為Web的基礎(chǔ)及根本:從一頁到另一頁的鏈接。這些鏈接都是URL。
通用URL語法
我敢說每個人在其一生中至少見過一次URL。比如”http://www.google.com”,就是一個URL。一個URL是一個統(tǒng)一資源定位器,事實上它指向了一個網(wǎng)頁(大多數(shù)情況下)。實際上,自從1994年的第一版規(guī)范開始,URL就有了一個良好定義的結(jié)構(gòu)。
我們能從”http://www.google.com”這個URL中讀出下列詳細信息:
Part | Data |
Scheme | http |
Hostaddress |
www.google.com |
如果我們看一個更復(fù)雜的URL,比如”https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third”我們就能獲取到下列信息:
Part | Data |
Scheme | https |
User | bob |
Password | bobby |
Hostaddress | www.lunatech.com |
port | 8080 |
Path | /file |
Pathparameters | p=1 |
Queryparameters | q=2 |
Fragment | third |
協(xié)議(即scheme,如上面的http和https(安全HTTP))定義了URL中其余部分的結(jié)構(gòu)。大多數(shù)互聯(lián)網(wǎng)URL協(xié)議擁有通用的開頭,包括用戶,密碼,主機名和端口,后面才是每個協(xié)議具體的部分。這個通用的部分負責(zé)處理認證,同時它也有能力知道為了請求數(shù)據(jù)應(yīng)該鏈接到哪兒。
HTTP URL語法
對于HTTP URL(使用http或https協(xié)議),URL的scheme描述部分定義了數(shù)據(jù)的路徑(path),后面是可選的query和fragment。
path部分看上去是一個分層的結(jié)構(gòu),類似于文件系統(tǒng)中文件夾和文件的分層結(jié)構(gòu)。path由”/”字符開始,每一個文件夾由”/”分隔,最后是文件。例如”/photos/egypt/cairo/first.jpg”有四個路徑片段(segment):”photos”、”egypt”、”cairo”和”first.jpg”,可以由此推出:”first.jpg”文件在文件夾”cairo”中,而”egypt”文件夾位于web站點的根文件夾”photos”里面。
每一個path片段可以有可選的path參數(shù)(也叫matrix參數(shù)),這是在path片段的最后由”;”開始的一些字符。每個參數(shù)名和值由”=”字符分隔,像這樣:”/file;p=1″,這定義了path片段”file”有一個path參數(shù)”p”,其值為”1″。這些參數(shù)并不常用—這得清楚—但是它們確實是存在,而且從YahooRESTfulAPI文檔我們能找到很好的理由去使用它們:
Matrix參數(shù)可以讓程序在GET請求中可以獲取部分的數(shù)據(jù)集。參考數(shù)據(jù)集的分頁。因為matrix參數(shù)可以跟任何數(shù)據(jù)集的URI格式的path片段,它們可以在內(nèi)部的path片段中被使用。
在路徑(path)部分之后是查詢(query)部分,它和路徑之間由一個“?”隔開,查詢部分包含了一個由“&”分隔開的參數(shù)列表,每一個參數(shù)由參數(shù)名稱、“=”號以及參數(shù)值組成。比如”/file?q=2″定義了一個查詢參數(shù)”q”,它的值是”2″。這在提交HTML表單時,或者當你使用諸如Google搜索等應(yīng)用時,用的非常多。
一個HTTPURL的最后部分是一個段落(fragment)部分,用來指向HTML文件中具體的某個部分,而不是整個HTML頁面。比如說,當你點擊鏈接時瀏覽器自動滾屏到某個部分而不是從頁面最頂部開始展示,就說明你點擊了一個擁有段落部分的URL。
URL語法
http URL方案最初由RFC1738定義(實際上,在之前的RFC1630也有涉及),而在httpURL方案被重新定義之前,整個URL語法就已經(jīng)由擴展了幾次以適應(yīng)發(fā)展的規(guī)范進化為一套統(tǒng)一資源標識符(UniformResourceIdentifiers即URIs)。
對于URLs如何拼裝,各部分如何分隔有一套語法。例如:”://”分隔方案和主機部分。主機同路徑片段部分由”/”分隔,而查詢部分緊跟在”?”之后。這意味著有些字符為語法保留。有些為整個URIs保留,而有些則被特定方案保留。所有出現(xiàn)在不應(yīng)出現(xiàn)位置的保留符(例如路徑片段——以文件名為例——可能包含”?”)必須被URL編碼。
URL編碼將字符轉(zhuǎn)變成對URL解析無意義的無害形式。它將字符轉(zhuǎn)化成為一種特定字符編碼的字節(jié)序列,然后將字節(jié)轉(zhuǎn)換為16進制形式,并將其前面加上”%”。問號的URL編碼形式為”%3F”。
我們可以將指向”to_be_or_not_to_be?.jpg”圖片的URL寫成:”http://example.com/to_be_or_not_to_be%3F.jpg”,這樣就沒有人會認為這兒可能由一個查詢部分了。
現(xiàn)今多數(shù)瀏覽器顯示URLs前都會對其解碼(將百分號編碼字節(jié)轉(zhuǎn)回其原本字符),并在獲取其網(wǎng)絡(luò)資源的時候重新編碼。這樣一來,很多用戶從未意識到編碼的存在。
另一方面,網(wǎng)頁作者,開發(fā)者必須明確認識到這一點,因為這里存在著很多陷阱。
URL常見陷阱
如果你正和URL打交道,了解下能夠避免的常見陷阱絕對是值得的?,F(xiàn)在我們給大家介紹下不僅限于此的一些常見陷阱。
使用哪類字符編碼?
URL編碼規(guī)范并沒有定義使用何種字符編碼形式去編碼字節(jié)。一般的ASCII字母數(shù)字字符并不需要轉(zhuǎn)義,但是ASCII之外的保留字需要(例如法語單詞“n?ud”中的”?”)。我們必須提出疑問,應(yīng)該使用哪類字符編碼來編碼URL字節(jié)。
當然如果只有Unicode的話,這個世界就會清凈很多。因為每個字符都包含其中,但是它只是一個集合,或者說是列表如果你愿意,它本身并不是一中編碼。Unicode可以使用多種方式進行編碼,譬如UTF-8或者UTF-16(也有其它格式),但是問題并沒有解決:我們應(yīng)該使用哪類字符來編碼URL(通常也指URI)。
標準并沒有定義一個URI應(yīng)該以何種方式指定其編碼,所以其必須從環(huán)境信息中進行推導(dǎo)。對于HTTPURL,它可以是HTML頁面的編碼格式,或HTTP頭的。這通常會讓人迷惑,也是許多錯誤的根源。事實上,最新版的URI標準定義了新的URIscheme將采用UTF-8,host(甚至已有的scheme)也使用UTF-8,這讓我更加懷疑:難道host和path真的可以使用不同的編碼方式?
每一部分的保留字都是不同。
是的,他們是,是的,他們,是的,他們是。。。
對于一個httpd連接,路徑片段部分中的空格被編碼為”%20″(不,完全沒有”+”),而“+”字符在路徑片段部分可以保持不編碼。
現(xiàn)在,在查詢部分,一個空格可能會被編碼為“+”(為了向后兼容:不要試圖在URI標準去搜索他)或者“%20”,當作為“+”字符(作為個統(tǒng)配符的結(jié)果)會被編譯為“%2B”。
這意味著“blue+lightblue”字串,如果在路徑部分或者查詢部分,將會有不同的編碼。比如得到”http://example.com/blue+light%20blue?blue%2Blight+blue”這樣的編碼形式,這樣我們不需從語法上分析url結(jié)構(gòu),就可以推導(dǎo)這個url的整個結(jié)構(gòu)是可能
考慮如下組裝URL的Java代碼片段
view sourceprint?
1 Stringstr=”blue+lightblue”;
2 Stringurl=”http://example.com/”+str+”?”+str;
編碼URL并不是為了轉(zhuǎn)義保留字而進行的簡單字符迭代,我們需要確切的知道哪個URL部份有哪些保留字,而有針對性的進行編碼。
這也意味著URL重寫過濾器如果不考慮合適的編碼細節(jié)而對URL直接進行分段轉(zhuǎn)換通常是有問題的。對URL進行編碼而不考慮具體的分段規(guī)則是不切實際的。
保留字不是你想象的那樣
大多數(shù)人不知道”+”在路徑部分是被允許的并且特指正號而不是空格。其他類似的有:
“?”在查詢部分允許不被轉(zhuǎn)義,
“/”在查詢部分允許不被轉(zhuǎn)義,
“=”在作為路徑參數(shù)或者查詢參數(shù)值以及在路徑部分允許不被轉(zhuǎn)義,
“:@-._~!$&'()*+,;=”等字符在路徑部分允許不被轉(zhuǎn)義,
“/?:@-._~!$&'()*+,;=”等字符在任何段中允許不被轉(zhuǎn)義。
這樣下面的地址雖然看起來有點混亂:”http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,
==?/?:@-._~!$'()*+,;=/?:@-._~!$'()*+,;==#/?:@-._~!$&'()*+,;=”
按照上面的規(guī)則,其實上是一個合法的地址。
不用奇怪,上面路徑可以被解析為:
部分 | 值 |
協(xié)議 | http |
主機 | example.com |
路徑 | /:@-._~!$&'()*+,= |
路徑參數(shù)名 | :@-._~!$&'()*+, |
路徑參數(shù)值 | :@-._~!$&'()*+,== |
查詢參數(shù)名 |
/?:@-._~!$'()*,; |
查詢參數(shù)值 | /?:@-._~!$'()*,;== |
段 | /?:@-._~!$&'()*+,;= |
不能分析解碼后的URL
URL的語法只在它被解碼前是有意義的,一旦解碼就可能出現(xiàn)保留字。
例如”http://example.com/blue%2Fred%3Fand+green”在解碼前由如下部分組成:
Part | Value |
Scheme | http |
Host | example.com |
Path segment | blue%2Fred%3Fand+green |
DecodedPathsegment | blue/red?and+green |
這樣看來,我們是在請求一個名為”blue/red?and+green”的文件,而不是一個位于”blue”文件夾下的名為”red?and+green”的文件。
如果我們把它解碼為”http://example.com/blue/red?and+green”,我們將得到如下部分:
Part | Value |
Scheme | http |
Host | example.com |
Path segment | blue |
Path segment | red |
Query parameter name | and green |
這明顯是錯誤的,所以,對保留字和URL各部分的分析必須在URL解碼之前完成。這意味著URL重寫過濾器不應(yīng)當在嘗試匹配之前解碼URL,當且僅當保留字允許進行URL編碼時才可以(有時符合這種情形,有時不符合,這取決于你的應(yīng)用)。
解碼后的URL不能被再編碼為同樣的形式
如果你解碼”http://example.com/blue%2Fred%3Fand+green”為”http://example.com/blue/red?and+green”,然后對它進行編碼(哪怕使用一個對URL每一部分都很了解的編碼器),你將會得到”http://example.com/blue/red?and+green”,這是因為它已經(jīng)是一個有效的URL。它跟我們解碼之前的URL非常的不同。
用Java正確處理URL
當你覺得自己已經(jīng)拿到了URL的黑腰帶(柔道中的最高級別–譯者注),你將會發(fā)現(xiàn)仍有一些Java里特有的、URL相關(guān)的陷阱。如果沒有一個強大的心臟,你很難正確的處理URL。
不要用java.net.URLEncoder或者java.net.URLDecoder來處理整個URL
不開玩笑。這些類不是用來編碼或解碼URL的,API文檔中清楚的寫著:
Utility class for HTML form encoding. This class contains static methods for converting a String to the application/x-www-form-urlencodedMIME format.For more information about HTML form encoding,consult the HTML specification.
這不是給URL用的。充其量它類似于查詢部分的編碼方式。使用它來編碼或解碼整個URL是錯誤的。你肯定以為標準的JDK一定會有一個標準的類來正確的處理URL編碼(是這樣,只不過是各部分分開處理的),但是要么是壓根沒有,要么是我們還沒有發(fā)現(xiàn)。不過,這種臆測導(dǎo)致許多人錯用了URLEncoder。
在對每一部分編碼之前不要拼裝URL
正如我們已經(jīng)講過的:完整構(gòu)建后的URL不能再被編碼。
以下面的代碼為例:
1 StringpathSegment=”a/b?c”
2 Stringurl=”http://example.com/”+pathSegment;
如果”a/b?c”是一個路徑片段,那么不可能把”http://example.com/a/b?c”轉(zhuǎn)換回之前它的原樣,因為它碰巧是一個有效的URL。之前我們已經(jīng)解釋過這一點。
下面是正確的代碼:
1 StringpathSegment=”a/b?c”
2 Stringurl=”http://example.com/
3 +URLUtils.encodePathSegment(pathSegment);
這里我們使用了一個工具類URLUtils,它是我們自己開發(fā)的,因為網(wǎng)絡(luò)上找不到一個詳盡的足夠快的工具類。上面的代碼會帶給你正確編碼的URL”http://example.com/a%2Fb%3Fc”。
注意,同樣的方式也適用于查詢子串:
1 Stringvalue=”a&b==c”;
2 Stringurl=”http://example.com/?query=”+value;
這會給你”http://example.com/?query=a&b==c”,這是個有效的URL,而不是我們想得到的http://example.com/?query=a%26b==c。
不要期望URI.getPath()給你結(jié)構(gòu)化的數(shù)據(jù)
因為一旦一個URL被解碼,句法信息就會丟失,下面這樣的代碼就是錯誤的:
1 URIuri=newURI(“http://example.com/a%2Fb%3Fc”)
2 for(StringpathSegment:uri.getPath().split(“/”))
3 System.err.println(pathSegment);
它會先將路徑”a%2Fb%3Fc”解碼為”a/b?c”,然后在不應(yīng)該分割的地方將地址分割為地址片段。
正確的代碼使用的是未解碼的路徑:
1 URIuri=newURI(“http://example.com/a%2Fb%3Fc”);
2
3 for(StringpathSegment:uri.getRawPath().split(“/”))
4 System.err.println(URLUtils.decodePathSegment(pathSegment));
注意路徑參數(shù)仍然存在:如果需要的話再處理它們。
不要期望Apache Commons HTTP Client的URI類能夠正確的做對
Apache Commons HTTP Client3的URI類使用了Apache Commons Codec的URLCodec來做URL編碼,正如API文檔提到的它是有問題的,因為它犯了和使用java.net.URLEncoder同樣的錯誤。它不但使用了錯誤的編碼器,還錯誤的按照每一部分都具有同樣的預(yù)定設(shè)置進行解碼。
在web應(yīng)用的每一層修復(fù)URL編碼問題
近來我們已經(jīng)被動修復(fù)了許多應(yīng)用中的URL編碼問題。從在Java中支持它,到低層次的URL重寫。這里我們會列出一些必要的修改。
總是在創(chuàng)建的時候進行URL編碼
在我們的HTML文件中,我們將所有出現(xiàn):
1 varurl=”#{vl:encodeURL(contextPath+’/view/’+resource.name)}”;
的地方替換為:
1 varurl=”#{contextPath}/view/#{vl:encodeURLPathSegment(resource.name)}”;
查詢參數(shù)也是類似的。
確保你的URL-rewrite過濾器正確的處理網(wǎng)址
Url重寫過濾器是一個重寫過濾器,我們在seam中用于轉(zhuǎn)化漂亮的地址去應(yīng)用依賴的網(wǎng)址。
例如,我們用它把http://beta.visiblelogistics.com/view/resource/FOO/bar轉(zhuǎn)化為http://beta.visiblelogistics.com/resources/details.seam?owner=FOO&name=bar。
很明顯,這個過程包含了一些字符串從一個地址到另一個地址,這意味著我們要從路徑部分解碼并且把它重新編碼為另一個查詢值部分。
我們起初的規(guī)則,如下所示:
1<urlrewrite decode-using=”utf-8″>
2<rule>
3
4<to encode=”false”>/resources/details.seam?owner=$1&name=$2</to>
5 </rule>
6</urlrewrite>
從這我們可以看到在重寫過濾器中只有兩種方法處理網(wǎng)址重寫:每一個的網(wǎng)址先被解碼去做規(guī)則匹配(模式),或者它不可用,所有規(guī)則去處理解碼。在我們看來后者是比較好的選擇,特別是當你要移動網(wǎng)址部分周圍,或者想去包含URL解碼路徑分隔符的匹配路徑部分時候。
在替換模式中(模式)你可以使用內(nèi)建的函數(shù)escape(String)和unescape(String)處理網(wǎng)站轉(zhuǎn)碼和解碼。
在撰寫這個文章的時候,UrlRewriteFilterBeta3.2有一些bugs,限制住我們提高URL-correctness:
網(wǎng)址解碼使用java.net.URLDecoder(這是錯誤的),
escape(String)和unescape(String)內(nèi)建函數(shù)使用java.net.URLDecoder和java.net.URLEncoder(不夠強大,只能用于這個查詢字串,所有的”&”或者”=”不被轉(zhuǎn)碼)。
We the refore made a big patch fixing a few issues like URL decoding,and adding the inline functionsescapePathSegment(String)andunescapePathSegment(String).
我們因此做了一個大修正補丁,用于修正諸如網(wǎng)址解碼問題以及增加內(nèi)建函建escapePathSegment(String)和unescapePathSegment(String)
我們現(xiàn)在可以這樣寫,幾乎不會有錯誤
1 <urlrewrite decode-using=”null”>
2 <rule>
3 <from>^/view/resource/(.*)/(.*)$</from>
4 <– Line breaks inserted for readability –>
5 <to encode=”false”>/resources/details.seam
6 ?owner=${escape:${unescapePath:$1}}
7 &name=${escape:${unescapePath:$2}}</to>
8 </rule>
9 </urlrewrite>
唯一可能出問題的地方是由于我們的補丁還不能解決以下的問題:
內(nèi)建的escaping/unescaping函數(shù)應(yīng)能只能編碼,這已經(jīng)做為下一個補丁(已經(jīng)做完了),或者能從http請求來確定(還不支持),
oldescape(String)和unescape(String)內(nèi)建函數(shù)被保留了,并且仍然調(diào)用java.net.URLDecoder,而這個包在由于沒有解決”&”和”=”的問題,所以仍然有問題,
我需要增加更多的局部特定的編碼和解碼函數(shù),
我們需要增加一個方法去鑒別per-rule解碼行為,對照全局在。
我們一有時間,我們就會發(fā)布第二個補丁。
正確使用Apachemod-rewrite
Apachemod-rewrite是一個ApacheWeb服務(wù)器的網(wǎng)址重寫模塊。例如用它來把http://beta.visiblelogistics.com/foo的流量代理到http://our-internal-server:8080/vl/foo。
這是最后的要修正的事情,就像是UrlRewriteFilter,他默認解碼網(wǎng)址給我們,并且從新編碼重寫過得網(wǎng)址給我們,這其實上是錯誤的,因為”解碼的網(wǎng)址不能被重新編碼”。
有一種方法可以避免這種行為,至少在我們的案例中我們沒有轉(zhuǎn)化一個網(wǎng)址部分到另一個網(wǎng)址,例如,我們不需要解碼一個路徑部分并且重新編碼它到一個查詢部分:沒有加碼也沒有重編碼。
我們通過THE_REQUEST來網(wǎng)址匹配來完成工作。他是完全的HTTP請求(包括HTTP方法和版本)聯(lián)合解碼。我們只要取host后面的URL部分,改變host和預(yù)設(shè)的/v/前綴和tada
…
#ThisisrequiredifwewanttoallowURL-encodedslashesapathsegment
AllowEncodedSlashesOn
#Enablemod-rewrite
RewriteEngineon
#UseTHE_REQUESTtonotdecodetheURL,sincewearenotmoving
#anyURIparttoanotherpartsowedonotneedtodecode/reencode
RewriteCond%{THE_REQUEST}”^[a-zA-Z]+/(.*)HTTP/\d\.\d$”RewriteRule^(.*)$http://our-internal-server:8080/vl/%1[P,L,NE]
結(jié)論
我希望闡明一些URL技巧和常見的錯誤。簡而言之,能把它說明白就夠了,但這不是一些人想象的那樣簡單的。我們展示了java常見的錯誤和一個web應(yīng)用部署的整個過程?,F(xiàn)在每個讀者都應(yīng)該是一個URL專家了,并且我們希望不要在看見相關(guān)bugs再出現(xiàn)。請求SUN公司,請為URLencoding/decoding逐項的增加標準支持
來源:產(chǎn)品中國
-
廣告合作
-
QQ群號:4114653