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