HTTP 協議能夠說是開發者最熟悉的一個網絡協議,「簡單易懂」和「易於擴展」兩個特色讓它成爲應用最普遍的應用層協議。javascript
雖然有諸多的優勢,可是在協議定義時由於諸多的博弈和限制,仍是隱藏了很多暗坑,讓人一不當心就會陷入其中。本文總結了 HTTP 規範中常見的幾個暗坑,但願你們開發中有意識的規避它們,提高開發體驗。html
Referer
HTTP 標準把 Referrer
寫成 Referer
(少些了一個 r
),能夠說是計算機歷史上最著名的一個錯別字了。前端
Referer
的主要做用是攜帶當前請求的來源地址,經常使用在反爬蟲和防盜鏈上。前段時間鬧的沸沸揚揚的新浪圖牀掛圖事件,就是由於新浪圖牀忽然開始檢查 HTTP Referer
頭,非新浪域名就不返回圖片,致使不少蹭流量的中小博客圖都掛了。java
雖然 HTTP 標準裏把 Referer
寫錯了,可是其它能夠控制 Referer
的標準並無將錯就錯。node
例如禁止網頁自動攜帶 Referer
頭的 <meta> 標籤,相關關鍵字拼寫就是正確的:webpack
<!-- 全局禁止發送 referrer -->
<meta name="referrer" content="no-referrer" />
複製代碼
還有一個值得注意的是瀏覽器的網絡請求。從安全性和穩定性上考慮,Referer
等請求頭在網絡請求時,只能由瀏覽器控制,不能直接操做,咱們只能經過一些屬性進行控制。好比說 Fetch 函數,咱們能夠經過 referrer
和 referrerPolicy
控制,而它們的拼寫也是正確的:nginx
fetch('/page', {
headers: {
"Content-Type": "text/plain;charset=UTF-8"
},
referrer: "https://demo.com/anotherpage", // <-
referrerPolicy: "no-referrer-when-downgrade", // <-
});
複製代碼
凡是涉及到 Referrer 的,除了 HTTP 字段是錯的,瀏覽器的相關配置字段拼寫都是正確的。git
%20
仍是 +
?這個是個史詩級的大坑,我曾經被這個協議衝突坑了一天。github
開始講解前先看個小測試,在瀏覽器裏輸入 blank test
( blank
和 test
間有個空格),咱們看看瀏覽器如何處理的:web
從動圖能夠看出瀏覽器把空格解析爲一個加號「+」。
是否是感受有些奇怪?咱們再作個測試,用瀏覽器提供的幾個函數試一下:
encodeURIComponent("blank test") // "blank%20test"
encodeURI("q=blank test") // "q=blank%20test"
new URLSearchParams("q=blank test").toString() // "q=blank+test"
複製代碼
代碼是不會說謊的,其實上面的結果都是正確的,encode 結果不同,是由於 URI 規範和 W3C 規範衝突了,纔會搞出這種讓人疑惑的烏龍事件。
咱們首先看看 URI 中的保留字,這些保留字不參與編碼。保留字符一共有兩大類:
:
/
?
#
[
]
@
!
$
&
'
(
)
*
+
,
;
=
URI 的編碼規則也很簡單,先把非限定範圍的字符轉爲 16 進制,而後前面加百分號。
空格這種不安全字符轉爲十六進制就是 0x20,前面再加上百分號 %
就是 %20
:
因此這時候再看 encodeURIComponent
和 encodeURI
的編碼結果,就是徹底正確的。
既然空格轉爲%20
是正確的,那轉爲 +
是怎麼回事?這時候咱們就要了解一下 HTML form 表單的歷史。
早期的網頁沒有 AJAX 的時候,提交數據都是經過 HTML 的 form 表單。form 表單的提交方法能夠用 GET 也能夠用 POST,你們能夠在 MDN form 詞條上測試:
通過測試咱們能夠看出表單提交的內容中,空格都是轉爲加號的,這種編碼類型就是 application/x-www-form-urlencoded
,在 WHATWG 規範裏是這樣定義的:
到這裏基本上就破案了,URLSearchParams
作 encode 的時候,就按這個規範來的。我找到了 URLSearchParams
的 Polyfill 代碼,裏面就作了 %20
到 +
的映射:
replace = {
'!': '%21',
"'": '%27',
'(': '%28',
')': '%29',
'~': '%7E',
'%20': '+', // <= 就是這個
'%00': '\x00'
}
複製代碼
規範裏對這個編碼類型還有解釋說明:
The
application/x-www-form-urlencoded
format is in many ways an aberrant monstrosity, the result of many years of implementation accidents and compromises leading to a set of requirements necessary for interoperability, but in no way representing good design practices. In particular, readers are cautioned to pay close attention to the twisted details involving repeated (and in some cases nested) conversions between character encodings and byte sequences. Unfortunately the format is in widespread use due to the prevalence of HTML forms.這種編碼方式就不是個好的設計,不幸的是隨着 HTML form 表單的普及,這種格式已經推廣開了
其實上面一大段句話就是一個意思:這玩意兒設計的就是 💩,但積重難返,你們仍是忍一下吧
URI 規範裏,空格 encode 爲 %20
, application/x-www-form-urlencoded
格式裏,空格 encode 爲 +
實際業務開發時,最好使用業內成熟的 HTTP 請求庫封裝請求,這些雜活兒累活兒框架都幹了;
若是非要使用原生 AJAX 提交 application/x-www-form-urlencoded
格式的數據,不要手動拼接參數,要用 URLSearchParams
處理數據,這樣能夠避免各類噁心的編碼衝突。
X-Forwarded-For
拿到的就是真實 IP 嗎?在這個小節開始前,我先講一個開發中的小故事,能夠加深一下你們對這個字段的理解。
前段時間要作一個和風控相關的需求,須要拿到用戶的 IP,開發後灰度了一小部分用戶,測試發現後臺日誌裏灰度的用戶 IP 全是異常的,哪有這麼巧的事情。隨後測試發過來幾個異常 IP:
10.148.2.122
10.135.2.38
10.149.12.33
...
複製代碼
一看 IP 特徵我就明白了,這幾個 IP 都是 10 開頭的,屬於 A 類 IP 的私有 IP 範圍(10.0.0.0-10.255.255.255),後端拿到的確定是代理服務器的 IP,而不是用戶的真實 IP。
如今有些規模的網站基本都不是單點 Server 了,爲了應對更高的流量和更靈活的架構,應用服務通常都是隱藏在代理服務器以後的,好比說 Nginx。
加入接入層後,咱們就能比較容易的實現多臺服務器的負載均衡和服務升級,固然還有其餘的好處,好比說更好的內容緩存和安全防禦,不過這些不是本文的重點就不展開了。
網站加入代理服務器後,除了上面的幾個優勢,同時引入了一些新的問題。好比說以前的單點 Server,服務器是能夠直接拿到用戶的 IP 的,加入代理層後,如上圖所示,(應用)原始服務器拿到的是代理服務器的 IP,我前面講的故事的問題就出在這裏。
Web 開發這麼成熟的領域,確定是有現成的解決辦法的,那就是 X-Forwarded-For 請求頭。
X-Forwarded-For
是一個事實標準,雖然沒有寫入 HTTP RFC 規範裏,從普及程度上看其實能夠算 HTTP 規範了。
這個標準是這樣定義的,每次代理服務器轉發請求到下一個服務器時,要把代理服務器的 IP 寫入 X-Forwarded-For
中,這樣在最末端的應用服務收到請求時,就會獲得一個 IP 列表:
X-Forwarded-For: client, proxy1, proxy2
複製代碼
由於 IP 是一個一個依次 push 進去的,那麼第一個 IP 就是用戶的真實 IP,取來用就行了。
可是,事實有這麼簡單嗎?
從安全的角度上考慮,整個系統最不安全的就是人,用戶端都是最好攻破最好僞造的。有些用戶就開始鑽協議的漏洞:X-Forwarded-For
是代理服務器添加的,若是我一開始請求的 Header 頭裏就加了 X-Forwarded-For
,不就騙過服務器了嗎?
1. 首先從客戶端發出請求,帶有 X-Forwarded-For
請求頭,裏面寫一個僞造的 IP:
X-Forwarded-For: fakeIP
複製代碼
2. 服務端第一層代理服務收到請求,發現已經有 X-Forwarded-For
,誤把這個請求當成代理服務器,因而向這個字段追加了客戶端的真實 IP:
X-Forwarded-For: fakeIP, client
複製代碼
3. 通過幾層代理後,最終的服務器拿到的 Header 是這樣的:
X-Forwarded-For: fakeIP, client, proxy1, proxy2
複製代碼
要是按照取 X-Forwarded-For
第一個 IP 的思路,你就着了攻擊者的道了,你拿到的是 fakeIP,而不是 client IP。
服務端如何破招?上面三個步驟:
第二步的破招我拿 Nginx 服務器舉例。
咱們在最外層的 Nginx 上,對 X-Forwarded-For
的配置以下:
proxy_set_header X-Forwarded-For $remote_addr;
複製代碼
什麼意思呢?就是最外層代理服務器不信任客戶端的 X-Forwarded-For
輸入,直接覆蓋,而不是追加。
非最外層的 Nginx 服務器,咱們配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
複製代碼
$proxy_add_x_forwarded_for
就是追加 IP 的意思。經過這招,就能夠破除用戶端的僞造辦法。
第三步的破招思路也很容易,正常思路咱們是取X-Forwarded-For
最左側的 IP,此次咱們反其道而行之,從右邊數,減去代理服務器的數目,那麼剩下的 IP 裏,最右邊的就是真實 IP。
X-Forwarded-For: fakeIP, client, proxy1, proxy2
複製代碼
好比說咱們已知代理服務有兩層,從右向左數,把 proxy1
和 proxy2
去掉,剩下的 IP 列表最右邊的就是真實 IP。
相關思路和代碼實現可參考 Egg.js 前置代理模式。
經過 X-Forwarded-For
獲取用戶真實 IP 時,最好不要取第一個 IP,以防止用戶僞造 IP。
HTTP 請求頭字段若是涉及到多個 value 時,通常來講每一個 value 間是用逗號「,」分隔的,就連非 RFC 標準的 X-Forwarded-For
,也是用逗號分隔 value 的:
Accept-Encoding: gzip, deflate, br
cache-control: public, max-age=604800, s-maxage=43200
X-Forwarded-For: fakeIP, client, proxy1, proxy2
複製代碼
由於一開始用逗號分隔 value,後面想再用一個字段修飾 value 時,分隔符就變成了分號「;」,最典型的請求頭就是 Accept
了:
// q=0.9 修飾的是 application/xml,雖然它們之間用分號分隔
Accept: text/html, application/xml;q=0.9, */*;q=0.8
複製代碼
雖然 HTTP 協議易於閱讀,可是這個分隔符用的仍是很不符合常識的。按常理來講,分號的斷句語氣是強於逗號的,可是在 HTTP 內容協商的相關字段裏倒是反過來的。這裏的定義能夠看 RFC 7231,寫的仍是比較清楚的。
和常規認識不一樣,Cookie 其實不算 HTTP 標準,定義 Cookie 的規範是 RFC 6265,因此分隔符規則也不同了。規範裏定義的 Cookie 語法規則是這樣的:
cookie-header = "Cookie:" OWS cookie-string OWS
cookie-string = cookie-pair *( ";" SP cookie-pair )
複製代碼
多個 cookie 之間是用分號「;」分隔的,而不是逗號「,」。我隨便扒了個網站的 cookie,可見是用分號分隔的,這裏須要特別注意一下:
大部分 HTTP 字段的 value 分隔符是逗號「,」
Cookie 不屬於 HTTP 標準,分隔符是分號「;」
下面我要推薦個人幾篇文章:
最後推薦一下個人我的公衆號:「滷蛋實驗室」,平時會分享一些前端技術和數據分析的內容,你們感興趣的話能夠關注一波: