<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          細(xì)述HTTP 規(guī)范中的那些 “陰間”坑

          共 806字,需瀏覽 2分鐘

           ·

          2020-12-18 02:47

          HTTP 協(xié)議可以說是開發(fā)者最熟悉的一個(gè)網(wǎng)絡(luò)協(xié)議,「簡(jiǎn)單易懂」和「易于擴(kuò)展」兩個(gè)特點(diǎn)讓它成為應(yīng)用最廣泛的應(yīng)用層協(xié)議。

          雖然有諸多的優(yōu)點(diǎn),但是在協(xié)議定義時(shí)因?yàn)橹T多的博弈和限制,還是隱藏了不少暗坑,讓人一不小心就會(huì)陷入其中。本文總結(jié)了 HTTP 規(guī)范中常見的幾個(gè)暗坑,希望大家開發(fā)中有意識(shí)的規(guī)避它們,提升開發(fā)體驗(yàn)。

          1.Referer

          HTTP 標(biāo)準(zhǔn)把 ?Referrer 寫成 Referer(少些了一個(gè) r),可以說是計(jì)算機(jī)歷史上最著名的一個(gè)錯(cuò)別字了。

          Referer 的主要作用是攜帶當(dāng)前請(qǐng)求的來源地址,常用在反爬蟲和防盜鏈上。前段時(shí)間鬧的沸沸揚(yáng)揚(yáng)的新浪圖床掛圖事件,就是因?yàn)樾吕藞D床突然開始檢查 HTTP Referer 頭,非新浪域名就不返回圖片,導(dǎo)致很多蹭流量的中小博客圖都掛了。

          雖然 HTTP 標(biāo)準(zhǔn)里把 Referer 寫錯(cuò)了,但是其它可以控制 Referer 的標(biāo)準(zhǔn)并沒有將錯(cuò)就錯(cuò)。

          例如禁止網(wǎng)頁自動(dòng)攜帶 Referer 頭的 標(biāo)簽,相關(guān)關(guān)鍵字拼寫就是正確的:


          <meta?name="referrer"?content="no-referrer"?/>

          還有一個(gè)值得注意的是瀏覽器的網(wǎng)絡(luò)請(qǐng)求。

          從安全性和穩(wěn)定性上考慮,Referer 等請(qǐng)求頭在網(wǎng)絡(luò)請(qǐng)求時(shí),只能由瀏覽器控制,不能直接操作,我們只能通過一些屬性進(jìn)行控制。比如說 Fetch 函數(shù),我們可以通過 referrerreferrerPolicy 控制,而它們的拼寫也是正確的:

          fetch('/page',?{
          ??headers:?{
          ????"Content-Type":?"text/plain;charset=UTF-8"
          ??},
          ??referrer:?"https://demo.com/anotherpage",?//?<-
          ??referrerPolicy:?"no-referrer-when-downgrade",?//?<-
          });

          一句話總結(jié):

          凡是涉及到 Referrer 的,除了 HTTP 字段是錯(cuò)的,瀏覽器的相關(guān)配置字段拼寫都是正確的。

          二.「靈異」的空格

          1.%20 還是 +

          這個(gè)是個(gè)史詩級(jí)的大坑,我曾經(jīng)被這個(gè)協(xié)議沖突坑了一天。

          開始講解前先看個(gè)小測(cè)試,在瀏覽器里輸入 blank testblanktest 間有個(gè)空格),我們看看瀏覽器如何處理的:

          從動(dòng)圖可以看出瀏覽器把空格解析為一個(gè)加號(hào)「+」。

          是不是感覺有些奇怪?我們?cè)僮鰝€(gè)測(cè)試,用瀏覽器提供的幾個(gè)函數(shù)試一下:

          encodeURIComponent("blank?test")?//?"blank%20test"
          encodeURI("q=blank?test")????????//?"q=blank%20test"
          new?URLSearchParams("q=blank?test").toString()?//?"q=blank+test"
          瀏覽器編碼規(guī)則

          代碼是不會(huì)說謊的,其實(shí)上面的結(jié)果都是正確的,encode 結(jié)果不一樣,是因?yàn)?URI 規(guī)范[1]W3C 規(guī)范[2]沖突了,才會(huì)搞出這種讓人疑惑的烏龍事件。

          2.沖突的協(xié)議

          我們首先看看 URI 中的保留字[3],這些保留字不參與編碼。保留字符一共有兩大類:

          • gen-delims:: / ? # [ ] @
          • sub-delims:! $ & ' ( ) * + , ; =

          URI 的編碼規(guī)則也很簡(jiǎn)單,先把非限定范圍的字符轉(zhuǎn)為 16 進(jìn)制,然后前面加百分號(hào)。

          空格這種不安全字符轉(zhuǎn)為十六進(jìn)制就是 0x20,前面再加上百分號(hào) % 就是 %20

          所以這時(shí)候再看 encodeURIComponentencodeURI 的編碼結(jié)果,就是完全正確的。

          既然空格轉(zhuǎn)為%20 是正確的,那轉(zhuǎn)為 + 是怎么回事?這時(shí)候我們就要了解一下 HTML form 表單的歷史。

          早期的網(wǎng)頁沒有 AJAX 的時(shí)候,提交數(shù)據(jù)都是通過 HTML 的 form 表單。form 表單的提交方法可以用 GET 也可以用 POST,大家可以在 MDN form 詞條[4]上測(cè)試:

          經(jīng)過測(cè)試我們可以看出表單提交的內(nèi)容中,空格都是轉(zhuǎn)為加號(hào)的,這種編碼類型就是 application/x-www-form-urlencoded,在 WHATWG 規(guī)范[5]里是這樣定義的:

          到這里基本上就破案了,URLSearchParams 做 encode 的時(shí)候,就按這個(gè)規(guī)范來的。我找到了 URLSearchParamsPolyfill 代碼[6],里面就做了 %20 到 ?+ 的映射:

          replace?=?{
          ????'!':?'%21',
          ????"'":?'%27',
          ????'(':?'%28',
          ????')':?'%29',
          ????'~':?'%7E',
          ????'%20':?'+',?//?<=?就是這個(gè)
          ????'%00':?'\x00'
          }

          規(guī)范里對(duì)這個(gè)編碼類型還有解釋說明:

          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.


          這種編碼方式就不是個(gè)好的設(shè)計(jì),不幸的是隨著 HTML form 表單的普及,這種格式已經(jīng)推廣開了

          其實(shí)上面一大段句話就是一個(gè)意思:這玩意兒設(shè)計(jì)的就是 ?,但積重難返,大家還是忍一下吧

          3.一句話總結(jié)

          • URI 規(guī)范里,空格 encode 為 %20application/x-www-form-urlencoded 格式里,空格 encode 為 +

          • 實(shí)際業(yè)務(wù)開發(fā)時(shí),最好使用業(yè)內(nèi)成熟的 HTTP 請(qǐng)求庫封裝請(qǐng)求,這些雜活兒累活兒框架都干了;

          • 如果非要使用原生 AJAX 提交 application/x-www-form-urlencoded 格式的數(shù)據(jù),不要手動(dòng)拼接參數(shù),要用 URLSearchParams 處理數(shù)據(jù),這樣可以避免各種惡心的編碼沖突。

          三.X-Forwarded-For 拿到的就是真實(shí) IP 嗎?

          1.故事

          在這個(gè)小節(jié)開始前,我先講一個(gè)開發(fā)中的小故事,可以加深一下大家對(duì)這個(gè)字段的理解。

          前段時(shí)間要做一個(gè)和風(fēng)控相關(guān)的需求,需要拿到用戶的 IP,開發(fā)后灰度了一小部分用戶,測(cè)試發(fā)現(xiàn)后臺(tái)日志里灰度的用戶 IP 全是異常的,哪有這么巧的事情。隨后測(cè)試發(fā)過來幾個(gè)異常 IP:

          10.148.2.122
          10.135.2.38
          10.149.12.33
          ...

          一看 IP 特征我就明白了,這幾個(gè) IP 都是 10 開頭的,屬于 A 類 IP 的私有 IP 范圍(10.0.0.0-10.255.255.255),后端拿到的肯定是代理服務(wù)器的 IP,而不是用戶的真實(shí) IP。

          2.原理

          現(xiàn)在有些規(guī)模的網(wǎng)站基本都不是單點(diǎn) Server 了,為了應(yīng)對(duì)更高的流量和更靈活的架構(gòu),應(yīng)用服務(wù)一般都是隱藏在代理服務(wù)器之后的,比如說 Nginx。

          加入接入層后,我們就能比較容易的實(shí)現(xiàn)多臺(tái)服務(wù)器的負(fù)載均衡和服務(wù)升級(jí),當(dāng)然還有其他的好處,比如說更好的內(nèi)容緩存和安全防護(hù),不過這些不是本文的重點(diǎn)就不展開了。

          網(wǎng)站加入代理服務(wù)器后,除了上面的幾個(gè)優(yōu)點(diǎn),同時(shí)引入了一些新的問題。比如說之前的單點(diǎn) Server,服務(wù)器是可以直接拿到用戶的 IP 的,加入代理層后,如上圖所示,(應(yīng)用)原始服務(wù)器拿到的是代理服務(wù)器的 IP,我前面講的故事的問題就出在這里。

          Web 開發(fā)這么成熟的領(lǐng)域,肯定是有現(xiàn)成的解決辦法的,那就是 ?X-Forwarded-For[7] 請(qǐng)求頭。

          X-Forwarded-For 是一個(gè)事實(shí)標(biāo)準(zhǔn),雖然沒有寫入 HTTP RFC 規(guī)范里,從普及程度上看其實(shí)可以算 HTTP 規(guī)范了。

          這個(gè)標(biāo)準(zhǔn)是這樣定義的,每次代理服務(wù)器轉(zhuǎn)發(fā)請(qǐng)求到下一個(gè)服務(wù)器時(shí),要把代理服務(wù)器的 IP 寫入 X-Forwarded-For 中,這樣在最末端的應(yīng)用服務(wù)收到請(qǐng)求時(shí),就會(huì)得到一個(gè) IP 列表:

          X-Forwarded-For: client, proxy1, proxy2

          因?yàn)?IP 是一個(gè)一個(gè)依次 push 進(jìn)去的,那么第一個(gè) IP 就是用戶的真實(shí) IP,取來用就好了。

          但是,事實(shí)有這么簡(jiǎn)單嗎?

          3.攻擊

          從安全的角度上考慮,整個(gè)系統(tǒng)最不安全的就是人,用戶端都是最好攻破最好偽造的。有些用戶就開始鉆協(xié)議的漏洞:X-Forwarded-For 是代理服務(wù)器添加的,如果我一開始請(qǐng)求的 Header 頭里就加了 X-Forwarded-For ,不就騙過服務(wù)器了嗎?

          1. 首先從客戶端發(fā)出請(qǐng)求,帶有 X-Forwarded-For 請(qǐng)求頭,里面寫一個(gè)偽造的 IP:

          X-Forwarded-For: fakeIP

          2. 服務(wù)端第一層代理服務(wù)收到請(qǐng)求,發(fā)現(xiàn)已經(jīng)有 X-Forwarded-For,誤把這個(gè)請(qǐng)求當(dāng)成代理服務(wù)器,于是向這個(gè)字段追加了客戶端的真實(shí) IP:

          X-Forwarded-For: fakeIP, client

          3. 經(jīng)過幾層代理后,最終的服務(wù)器拿到的 Header 是這樣的:

          X-Forwarded-For: fakeIP, client, proxy1, proxy2

          要是按照取 X-Forwarded-For 第一個(gè) IP 的思路,你就著了攻擊者的道了,你拿到的是 fakeIP,而不是 client IP。

          4.破招

          服務(wù)端如何破招?上面三個(gè)步驟:

          • 第一步是客戶端造假,服務(wù)器無法介入
          • 第二步是代理服務(wù)器,可控,可防范
          • 第三步是應(yīng)用服務(wù)器,可控,可防范

          第二步的破解我拿 Nginx 服務(wù)器舉例。

          我們?cè)谧钔鈱拥?Nginx 上,對(duì) X-Forwarded-For 的配置如下:

          proxy_set_header X-Forwarded-For $remote_addr;

          什么意思呢?就是最外層代理服務(wù)器不信任客戶端的 X-Forwarded-For 輸入,直接覆蓋,而不是追加

          非最外層的 Nginx 服務(wù)器,我們配置:

          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

          $proxy_add_x_forwarded_for 就是追加 IP 的意思。通過這招,就可以破解用戶端的偽造辦法。

          第三步的破解思路也很容易,正常思路我們是取X-Forwarded-For 最左側(cè)的 IP,這次我們反其道而行之,從右邊數(shù),減去代理服務(wù)器的數(shù)目,那么剩下的 IP 里,最右邊的就是真實(shí) IP。

          X-Forwarded-For: fakeIP, client, proxy1, proxy2

          比如說我們已知代理服務(wù)有兩層,從右向左數(shù),把 proxy1proxy2 去掉,剩下的 IP 列表最右邊的就是真實(shí) IP。

          相關(guān)思路和代碼實(shí)現(xiàn)可參考 Egg.js 前置代理模式[8]

          5.一句話總結(jié)總結(jié)

          通過 X-Forwarded-For 獲取用戶真實(shí) IP 時(shí),最好不要取第一個(gè) IP,以防止用戶偽造 IP。

          四.略顯混亂的分隔符

          1.HTTP 標(biāo)準(zhǔn)

          HTTP 請(qǐng)求頭字段如果涉及到多個(gè) value 時(shí),一般來說每個(gè) value 間是用逗號(hào)「,」分隔的,就連非 RFC 標(biāo)準(zhǔn)的 X-Forwarded-For,也是用逗號(hào)分隔 value 的:

          Accept-Encoding: gzip, deflate, br
          cache-control: public, max-age=604800, s-maxage=43200
          X-Forwarded-For: fakeIP, client, proxy1, proxy2

          因?yàn)橐婚_始用逗號(hào)分隔 value,后面想再用一個(gè)字段修飾 value 時(shí),分隔符就變成了分號(hào)「;」,最典型的請(qǐng)求頭就是 Accept 了:

          //  q=0.9 修飾的是 application/xml,雖然它們之間用分號(hào)分隔
          Accept: text/html, application/xml;q=0.9, */*;q=0.8

          雖然 HTTP 協(xié)議易于閱讀,但是這個(gè)分隔符用的還是很不符合常識(shí)的。按常理來說,分號(hào)的斷句語氣是強(qiáng)于逗號(hào)的,但是在 HTTP 內(nèi)容協(xié)商的相關(guān)字段里卻是反過來的。這里的定義可以看 RFC 7231[9],寫的還是比較清楚的。

          2.Cookie 標(biāo)準(zhǔn)

          和常規(guī)認(rèn)識(shí)不同,Cookie 其實(shí)不算 HTTP 標(biāo)準(zhǔn),定義 Cookie 的規(guī)范是 RFC 6265[10],所以分隔符規(guī)則也不一樣了。規(guī)范里定義的 Cookie 語法規(guī)則[11]是這樣的:

          cookie-header = "Cookie:" OWS cookie-string OWS
          cookie-string = cookie-pair *( ";" SP cookie-pair )

          多個(gè) cookie 之間是用分號(hào)「;」分隔的,而不是逗號(hào)「,」。我隨便扒了個(gè)網(wǎng)站的 cookie,可見是用分號(hào)分隔的,這里需要特別注意一下:

          3.一句話總結(jié)

          • 大部分 HTTP 字段的 value 分隔符是逗號(hào)「,」

          • Cookie 不屬于 HTTP 標(biāo)準(zhǔn),分隔符是分號(hào)「;」

          五、文章推薦

          下面我要推薦我的幾篇文章:

          • 一篇介紹了 webpack 中最易混淆的 5 個(gè)知識(shí)點(diǎn)[12],掘金快 800 贊了,一文講清楚 Webpack 中那些長(zhǎng)得像卻意義不同的概念
          • 一篇詳細(xì)介紹了 webpack dll 是個(gè)什么東西[13],并且給出了 2 條最佳實(shí)踐,擺脫繁瑣的 dll 配置
          • React Native 性能優(yōu)化指南[14]從渲染層的角度分析了 RN 性能優(yōu)化的 6 個(gè)點(diǎn),并以圖文形式講解了 FlatList 的實(shí)現(xiàn)原理
          • Web Scraper——輕量數(shù)據(jù)爬取利器 介紹了一個(gè)小巧的瀏覽器爬蟲插件,可以實(shí)現(xiàn)簡(jiǎn)單的數(shù)據(jù)爬取功能



          參考資料

          [1]

          URI 規(guī)范: https://tools.ietf.org/html/rfc3986

          [2]

          W3C 規(guī)范: https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1

          [3]

          保留字: https://tools.ietf.org/html/rfc3986#section-2.2

          [4]

          MDN form 詞條: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/form

          [5]

          WHATWG 規(guī)范: https://url.spec.whatwg.org/#urlencoded-serializing

          [6]

          Polyfill 代碼: https://github.com/WebReflection/url-search-params/blob/814161e99f1dd4453f3c1dc594bc73da2bd61838/build/url-search-params.node.js#L88

          [7]

          X-Forwarded-For: https://en.wikipedia.org/wiki/X-Forwarded-For

          [8]

          Egg.js 前置代理模式: https://eggjs.org/zh-cn/tutorials/proxy.html

          [9]

          RFC 7231: https://tools.ietf.org/html/rfc7231

          [10]

          RFC 6265: https://tools.ietf.org/html/rfc6265

          [11]

          Cookie 語法規(guī)則: https://tools.ietf.org/html/rfc6265#section-4.2.1

          [12]

          webpack 中最易混淆的 5 個(gè)知識(shí)點(diǎn): https://juejin.im/post/5cede821f265da1bbd4b5630

          [13]

          webpack dll 是個(gè)什么東西: https://juejin.im/post/5d8aac8fe51d4578477a6699

          [14]

          React Native 性能優(yōu)化指南: https://juejin.im/post/5e1676e16fb9a04847095b12

          ??看完三件事

          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)贊,讓更多的人也能看到介紹內(nèi)容(收藏不點(diǎn)贊,都是耍流氓-_-)
          2. 關(guān)注公眾號(hào)“前端勸退師”,不定期分享原創(chuàng)知識(shí)。
          3. 也看看其他文章

          勸退師個(gè)人微信:huab119

          也可以來我的GitHub博客里拿所有文章的源文件:

          前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀


          點(diǎn)贊、在看 支持作者??


          瀏覽 32
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  永久在线| 青青草原在线视频 | 91综合在线| 翔田千里无遮挡全棵 | 日韩三级片播放 |