Web開發者應知的URL編碼知識

原文出處:http://blog.jobbole.com/42246/javascript

 

本文首先闡述了人們關於統一資源定位符(URL)編碼的廣泛的誤讀,其後經過闡明HTTP場景下的URL encoding 來引出咱們常常遇到的問題及其解決方案。本文並不特定於某類編程語言,咱們在Java環境下闡釋問題,最後從Web應用的多個層次描述如何解決URL編碼的問題來結尾。html

簡介java

當咱們天天上網衝浪時,有一些技術咱們無時無刻不在面對。有數據自己(網頁),數據的格式化,可以讓咱們獲取數據的傳輸機制,以及讓Web網絡可以真正成爲Web的基礎及根本:從一頁到另外一頁的連接。這些連接都是URL。web

通用URL語法apache

我敢說每一個人在其一輩子中至少見過一次URL。好比」http://www.google.com」,就是一個URL。一個URL是一個統一資源定位器 ,事實上它指向了一個網頁(大多數狀況下)。實際上,自從1994年的初版規範開始,URL就有了一個良好定義的結構。編程

咱們能從」http://www.google.com」 這個URL中讀出下列詳細信息:api

Part Data
Scheme http
Host address www.google.com

若是咱們看一個更復雜的URL,好比 「https://bob:bobby@www.lunatech.com: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,如上面的httphttps (安全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編碼(是這樣,只不過是各部分分開處理的),可是要麼是壓根沒有,要麼是咱們尚未發現。不過,這種臆測致使許多人錯用了URLEncoder。

在對每一部分編碼以前不要拼裝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

1
2
3
4
5
6
7
8
9
10
11
12
...
 
# 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逐項的增長標準支持。

相關文章
相關標籤/搜索