http請求中加號被替換為空格?源碼背後的秘密

持續輸出原創文章,點擊藍字關注我吧

這是why技術的第20篇原創文章

本周本來是沒有時間寫技術文章的,原因在上次推文的時候說明了。(回憶錄跳票了,原因非常蛋疼,次條說明了原因。)

為了周更不斷,想着去把之前發佈在其他平台的一篇原創文章搬過來就行。結果發現,當年我寫的那篇文章,離真相還差着十萬八千里。

而去搜索這個問題時,我的文章是檢索結果的第一個

原文《http請求參數中加號被替換為空格及請求參數被URLDeCode的記錄》鏈接如下:

https://www.jianshu.com/p/1a30b585c39e

所以為了避免繼續誤導讀者,就算周末"爆肝",也得輸出此文,不得不發。

這是我作為程序員的自我修養。

加號變空格

之前寫那篇文章的原因是碰到了兩個有趣的問題,如下:

首先,我們進行場景復現,搭建項目的過程就不說了,用idea+springboot搭建一個簡單的web項目還不是信手拈來的事?

正如上面的現象所示:我的入參是jay+love,但是後台接收到的是jay love,加號變空格了。為什麼呢?

源碼之下無秘密

本文分析的Tomcat源碼版本為:9.0.29.

通過Debug可以找到兩處關鍵的代碼:

第一處:

org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset) 下圖中的290行

在這個地方因為有'+',所以把decodeValue參數設置為true,表示需要對請求中的value進行decode操作。

decode的具體的源碼位置如下,也就是第二處關鍵代碼:

org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean)

可以看到,在源碼裏面有一段代碼,是把'+'替換了為了空格,是特意做了這樣的特殊處理。

整個方法的解讀如下:

所以我的入參是jay+love,但是後台接收到的是jay love,加號變空格了。為什麼呢?

原因很簡單,在源碼中有一段代碼把'+'替換成了空格,刻意為之。

為什麼這樣做呢?

之前的文章裏面我寫的是:

由於歷史原因,那到底是什麼歷史原因呢?

我在網上查了一圈,沒有找到具體的歷史原因,我看到的所有的關於這個問題的文章,要麼只是給了解決方案,要麼就是上面這一句歷史原因,一帶而過,含糊其辭。

這裡,我就明明白白的告訴你為啥。

經過我長時間的摸排,我找到了很多蛛絲馬跡,整理之後,我決定從JDK的一個"BUG"講起。

對應鏈接:http://bugs.sun.com/view_bug.do?bug_id=4616184

從提交時間上可以看出,該問題早在2001年,距今18年前就有人指出來了,並給JDK上報了BUG,他的描述如下:

首先,我們先把他的測試代碼拿出來跑一下:

他為什麼說空格encode之後應該是%20呢?

因為他在BUG裏面提到了RFC2396標準。(RFC就不解釋了,你只要知道是業界認證的權威標準就行):

http://www.ietf.org/rfc/rfc2396.txt

在RFC2396的第2.4.1節,明確的說了:"%20"是US-ASCII空格字符的轉義編碼。

去查詢標準的ASCII碼你也可以發現確實是這樣的:

用代碼實踐一下,證明以上結論:

看java.net.URLEncoder#encode(java.lang.String, java.lang.String)的源碼也可以直觀的看到,源碼裏面做了特殊處理

再看java.net.URLDecoder#decode(java.lang.String, java.lang.String)的源碼:

這裡就和前面的呼應上了,這處理方式,一模一樣呀。所以為什麼這樣處理,兩處地方屬於同宗同源啊!

而提BUG的那個哥們為什麼覺得這是一個BUG呢?

雖然經過試驗,'+'和'%20'經過decode都能轉化為空格,但是他認為,根據RFC2396來講,這裡只能是'%20',怎麼能變成'+'呢?所以他覺得這是一個BUG。

那我們看看JDK官方是怎麼回復這個問題的呢?

官方回復:

這不是BUG啊,朋友!這個類就是遵循了HTML規範中的規定:如何對 HTML表單中的URLs進行encode。它不打算用於其他用途。 而這樣做的原因,是因為包括HTML 4.01第17.13.4節和RFC 1866(已經被W3C HTML推薦標準取代)都是這樣規定的。

對於第一段話,官方的意思我理解是:這個類就是拿來對url進行encode的,不做其他用途。因為你調用了encode編碼,那就需要decode解碼,我只要保證你解碼之後的數據和你encode之前的數據是一樣的就行了。你要拿去搞其他事情,我就管不了了。

而為什麼這樣做呢?是因為規定就是這樣的呀,類似於國家標準就是這樣的,類似於產品經理提出的需求就是這樣的呀。這裡官方提出了兩個標準,一個是HTML 4.01,一個是RFC1866(這個已經被其他的標準取代了,那我們就只看HTML 4.01)。

HTML4.01是1999年12月24日發佈的,在HTML4.0基礎上進行微小改進,W3C推薦標準 。 在w3c上找到該標準,地址如下 https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1

下圖圈起來的地方很關鍵,可以點開放大查看:

找到HTML 4.01第17.13.4節,其中明確指出:當content-type為application/x-www-form-urlencoded時,對names和vaules進行轉義,空格用'+'代替。

HTML 4.01第17.13.4節原文如下:

Control names and values are escaped. Space characters are replaced by `+'

官方舉的雖然是HTML 4.01的例子,但是我翻譯了歷史文獻,發現其實在更早的HTML 3.2規範中就規定了,HTML 3.2規範在1996年就成為了W3C推薦標準,其中相關內容如下:

鏈接地址:https://www.w3.org/TR/2018/SPSD-html32-20180315/

而application/x-www-form-urlencoded是瀏覽器默認的content-type。

在BUG裏面提到的RFC2396標準是1998年8月提出來的

HTML 3.2規範在1996年就成為了W3C推薦標準

所以,我覺得這就是歷史原因!

再說一次,在HTML 4.01規範中就明確規定了:當content-type為application/x-www-form-urlencoded時,對names和vaules進行轉義,空格用'+'代替。

沒有原因,就是規定!我在查詢的過程中發現,其他的編程語言也有這樣的問題,因為他們都遵從同樣的標準,就有了同樣的"歷史原因"。

回到前面的這個地方:

這裡解碼的時候為什麼把'+'轉化為空格呢?因為"歷史原因",如果URLs中出現了空格,需要用'+'替換,所以這裡解碼的時候把'+'轉化回了空格。先有了編碼的操作,所以才會有解碼的操作。

很多的文章都在說這是'+'的原因,甚至有的文章說'+'的編碼應該改為%20。但是其實上面分析過了,有問題的是空格,而不是'+'。

那為什麼我們在做表單提交的時候,也經常寫'+'號呀,為什麼沒有問題呢?

因為當Html的表單被提交時, 每個表單域都會被Url編碼之後才在被發送,下面的小例子可以佐證:

解決方案

解決方案網上一大堆了,我這裡羅列一下吧:

方案一:修改客戶端,將客戶端帶'+'的參數中的'+'全部替換為‍'%2B',如下:

方案二:修改服務器端,將空格替換為'+',這種方式只適用於參數中'+'沒有空格的情況。如下:

方案三:修改服務器端,將獲取參數的方法由‍reuqest.‍getParameter改為‍request.getQueryString(),然後對得到的字符串進行解析。

最後說一句

正如我文章最開始說的,就算是熬夜爆肝,我也必須得輸出這篇文章,因為我最開始的文章不僅寫的表面,而且還有一些問題,我得對其進行糾正。

讓我突然想起了之前和朋友的一次對話,他問我說:你作為程序員,時刻待命,只要系統一出問題你就立馬會響應。你不覺得累嗎?

我回答道:說真的,當系統出問題,需要我排查問題的時候,我不覺得累。因為這個系統是我負責的,代碼是我自己一行行的寫出來的。出現了問題,我得證明我的系統是沒有問題的,是不是別人的打開方式不對。但是如果真的是我的代碼導致的問題,我會心有愧疚,我也得立即響應,對其負責。

這是我作為一個程序員的自我修養。

這篇文章的風格和《這道面試題我真不知道面試官想要的回答是什麼》有點相似,全文描述的都是很小的知識點,甚至可以說是冷知識。一句話就能說出表面上的為什麼,提煉出一個知識點。

但是我覺得提煉出來的,是一個乾癟癟的知識點,它不夠豐富,沒有探索的過程。

而我所展示的是我去尋找這個問題的答案的過程。通過JDK的"BUG"把幾個協議串聯起來,而且是全世界共同遵循的協議,極具權威性。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。

如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

感謝您的閱讀,感謝您的關注。

以上。

以下還有往期精彩哦。原創不易,賞個"在看"吧。

點擊這裡,可以留言

持續輸出原創

長按識別關注

往期精彩

很開心,在使用mybatis的過程中我踩到一個坑。

訂閱號做了77天,我掙了xxx元。

參加Dubbo社區開發者日成都站後,帶給我的一些思考。 Dubbo加權輪詢負載均衡的源碼和Bug,了解一下?

Dubbo一致性哈希負載均衡的源碼和Bug,了解一下?

一文講透Dubbo負載均衡之最小活躍數算法

原創不易,點個贊吧