關於URL Encoding的那些事
- 2019 年 12 月 17 日
- 筆記
之前遇到一個關於URL encoding的一個問題,很tricky,這裡把這個問題的root cause以及對這個問題的一些思考記錄下來,分享給大家。
首先,抽象這個問題的原型如下:
有一個電商平台,我們需要調用其暴露的一個API來創建電商資訊,API要求我們把電商名作為URL參數傳過去,然後創建相應的一條電商記錄。這個API示例如下:
POST http://localhost:8080/stores/{storeName}返回結果:
{ "storeName": "xxx", "otherInfo": "" }在測試這個API的過程中發現,有一個電商名(abc{d)包含字元「{」,出現了一個問題:用postman發送請求過去能夠成功;但是通過java程式碼發送請求則報錯,提示說URL syntax出錯,URL不允許包含「{」字元。
POST http://localhost:8080/stores/abc{d後來發現,postman能成功的原因是由於postman自動幫忙做了個URL encoding,通過創建出來的那條記錄可以驗證,創建出來的記錄如下。
{ "storeName": "abc%7Bd", "otherInfo": "" }而java程式碼沒有做encoding,所以就報錯了。因為根據URL的RFC規範,「{」字元確實不允許,必須做encoding。See https://tools.ietf.org/html/rfc3986#section-2。
那麼,怎麼解決這個問題呢,如何允許傳入包含「{」字元的電商名呢?當時想到的解決方案是,在程式碼中顯式的把電商名都做一個URL encoding,然後再作為URL參數傳過去。很好,用這個方案把這個問題解決了。請求URL參數電商名是encoded過的(abc%7Bd),創建出來也是encoded過的(abc%7Bd)。
沒過多久,又出現了另外一個問題。在測試過程中,有一個電商名(abc:d)包含字元「:」,按照之前的邏輯,我們把「:」做一個encoding(abc%3Ad)發送過去,期望創建的記錄應該是encoded的名字,但是結果卻不是,結果是一個decoded的名字(abc:d),即包含「:」本身。
結果因為這個,程式出錯了。同時測試還發現,通過postman發送請求過去沒有問題,發送帶字元「:」的名字(abc:d)過去,創建出來的就是帶「:」(abc:d);發送字元「:」的encoded串(abc%3Ad)過去,得到的就是encoded的串(abc%3Ad)。但是,通過程式碼發送字元「:」(abc:d)或者字元「:」(abc%3Ad)的encoded的串,創建出來的都是「:」(abc:d)。到這裡就有點思路了,發現字元「:」好像在哪裡被自動decode了。
經過調試發現,問題出在一個底層依賴的library的版本上,java發送http請求的方法最終依賴於一個library叫做org.apache.httpcomponents:httpclient,高版本(4.5.8)的library會自動decode一些字元(包括「:」),低版本(4.5.5)的則不會。
那麼到這裡,問題就明白了,問題的root cause即是雖然我們顯式把字元「:」做了一個encoding,但是在請求被真正發出去之前被Httpclient自動decode了,所以創建出來的記錄都是「:」。
對這個問題的一些思考:
第一,為啥字元「:」的encoded串會被自動decode,而字元「{」卻沒有?
原因是,「:」是URL規範允許的字元,儘管其是保留字元。而「{」是不允許字元,必須做URL encoding,中文漢字也是一樣,必須做URL encoding。
第二,如果傳入的是一個中文字元的encoded串,創建出來的就是encoded的串,很顯然這個不易讀,所以我們需要把這個encoded的串做一個decode。那麼設想一下,服務端每次都需要顯示做decode嗎?記得之前在Spring mvc項目中沒有顯示decode啊?
原因是Spring mvc自動把URL參數都做了一個decoding,所以我們不用顯示decode。而這個問題中的API在實現端沒有利用自動decode功能,即拿的是原生的參數值,所以一些時候會存在不易讀。自動decoding會有什麼問題嗎?在大多數情況下應該是沒問題,即輸入「{」的encoded串,後端得到的是就是「{」。那麼如果本身參數值就是「{」的encoded串呢,也沒問題,發請求方自己需要對這個串做一次encoding作為輸入。
第三,發現自動decoding在不同技術棧平台(Spring boot / mvc, .net core / mvc, .net framework / mvc, Nodejs)實現不一樣 ,有時候也會出現不一致的情況。比如說,當請求的URL參數包含%3F(字元「?」的encoded串),在Spring boot和 .net core都能夠正常拿到字元「?」;在.net framework里卻會報錯。而當請求的URL參數包含%2F(字元「/」的encoded串),在Spring boot, .net core和 .net framework里都不工作;在Nodejs里,用相對比較原生的方式,就可以工作並且獲取到這個URL參數。如下:
P1:Spring boot中字元「?」是work的

P2:Spring boot中字元「/」不work,報404

P3:Nodejs中可以拿到包含字元「/」的參數

所以有時候利用平台的自動decoding可能會出現一些問題,這時候你可能需要考慮利用平台相對比較原生的方式操作httprequest對象,比如上面nodejs的方式。
最後,其實關於編碼,之前也寫過一篇關於utf8編碼的文章(關於編碼的那些事),這裡討論的是URL encoding。編碼,個人理解其本質就是把一種表現形式的內容通過某種方式轉換成另外一種形式,以達到某些目的,比如URL encoding把字元「{」轉成「%7B」,這樣才符合http規範,URL才能被正確解析及轉發到伺服器;比如utf8編碼把字元「漢」轉成「e6b189」,這樣才能被電腦存儲,因為電腦只能識別一個位元組一個位元組。除了utf8編碼、URL encoding,我們常用到的還有另外一種編碼方式:base64編碼,這個編碼主要用於混淆易讀的一些資訊,比如jwt token。
注意,我們在很多地方還用到哈希演算法,比如SHA1等,編碼和哈希最大的區別就是:編碼是可逆的,哈希不可逆。
References
- URL規範:https://tools.ietf.org/html/rfc3986#section-2
