HTTP 知識總結 | 青訓營筆記
theme: channing-cyan
這是我參與「第五屆青訓營 」伴學筆記創作活動的第 5 天
介紹
HTTP(超文字傳輸協議,英語:HyperText Transfer Protocol)是一個用於傳輸超媒體文件(例如 HTML)的應用層協議,是全球資訊網的資料通訊的基礎。
版本
- 1999年6月公佈的 RFC 2616,定義了 HTTP 協議中現今廣泛使用的一個版本 HTTP 1.1。
- 2015年5月以 RFC 7540 正式釋出 HTTP/2 標準,取代 HTTP 1.1 成為 HTTP 的實現標準。
- 2022年6月6日標準化為 RFC9114 的最新版本 HTTP/3,拋棄使用 TCP,通過 UDP 上使用 QUIC 來承載應用層資料。
通訊過程
- 使用 TCP 協議,通過網頁瀏覽器、網路爬蟲或者其它的工具,客戶端(user agent,使用者代理程式)發起一個 HTTP 請求到伺服器上指定埠(預設埠為80)。
- 應答的伺服器(origin server)上儲存著一些資源,比如 HTML 檔案和影象,伺服器在那個埠監聽客戶端的請求。在使用者代理和源伺服器中間可能存在多個“中間層”,比如代理伺服器、閘道器或者隧道(tunnel)。
- 一旦收到請求,伺服器會向客戶端返回一個狀態,比如"HTTP/1.1 200 OK",以及返回的內容,如請求的檔案、錯誤訊息、或者其它資訊。
- 每次連線只處理一個請求,伺服器處理完客戶的請求,並收到客戶的應答後,即斷開連線,採用這種方式可以節省傳輸時間。HTTP/2 中的連線具有複用性,即每個目標地址建立連線後,可以永久被利用,所以每個來源僅需要一個連線。
請求方法
它們都實現了不同的語義,但根據共同的特徵由可以分類為:safe(安全), idempotent(冪等), 或 cacheable(可快取)。 - GET 的請求應該只被用於獲取資料。 - HEAD 方法請求一個與 GET 請求的響應相同的響應,但沒有響應體。 - POST 方法用於將實體提交到指定的資源,通常導致在伺服器上的狀態變化或副作用。 - PUT 方法用請求有效載荷替換目標資源的所有當前表示。 - DELETE 方法刪除指定的資源。 - CONNECT 方法建立一個到由目標資源標識的伺服器的隧道。 - OPTIONS 方法用於描述目標資源的通訊選項。 - TRACE 方法沿著到目標資源的路徑執行一個訊息環回測試。 - PATCH 方法用於對資源應用部分修改。
Safe(安全)
- 指這是個不會修改伺服器的資料的方法,也就是說,這是一個對伺服器只讀操作的方法。
- 瀏覽器呼叫安全的方法不用考慮會給服務端造成什麼危害,這樣,服務端就能允許客戶端預載入資源。
- 這些方法是安全的:GET,HEAD 和 OPTIONS。所有安全的方法都是冪等的,但並非所有冪等方法都是安全的,例如,PUT 和 DELETE 都是冪等的,但不是安全的。
Idempotent(冪等)
- 指的是同樣的請求被執行一次與連續執行多次,客戶端接收到的結果都是一樣的,伺服器的狀態也是一樣的,也就是說,冪等方法不應該具有副作用(統計用途除外)。
- 在正確實現的條件下, GET、HEAD、OPTIONS、PUT 和 DELETE 等方法都是冪等的,而 POST 方法不是。所有的 safe 方法也都是冪等的。例如下面呼叫多次 POST 方法,就會增加多行記錄:
POST /add_row HTTP/1.1 POST /add_row HTTP/1.1 -> Adds a 2nd row POST /add_row HTTP/1.1 -> Adds a 3rd row
下面即使請求多次 DELETE 方法接收到的狀態碼不一樣,但也是冪等的:DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted DELETE /idX/delete HTTP/1.1 -> Returns 404
Cacheable(可快取)
- 可以被快取的 HTTP 響應,將被儲存以供以後檢索和使用。
- 請求中使用的方法本身是可快取的,即一個 GET 或一個 HEAD 方法。如果指示新鮮度並設定了 Content-Location 標頭,也可以快取對 POST 或 PATCH 請求的響應,但這很少實現。其他方法如 PUT 或 DELETE 不可快取,它們響應的結果也無法快取。
- 應用程式快取可以根據響應的狀態程式碼,認為它是可快取的。以下狀態程式碼是可快取的:200、203、204、206、300、301、404、405、410、414 和 501。
- 如果響應中有特定的標頭,如 Cache-Control,可防止快取。
HTTP 標頭(header)
- HTTP 標頭是用於 HTTP 請求或響應的欄位,它傳遞關於請求或者響應的額外上下文和元資料。
- 例如,請求訊息可以使用標頭表明它首選的媒體格式,而響應可以使用標頭表明返回主體的媒體格式。
- 標頭是不區分大小寫,開始於行首,後面緊跟著一個 ':' 和與之相關的值。欄位值在一個換行符(CRLF)前或者整個訊息的末尾結束。
- 根據不同的訊息上下文,標頭可以分為:
- 請求標頭:包含要獲取的資源或者客戶端自身的更多資訊。例如,Accept-* 標頭指示響應的允許格式和首選格式。其他標頭可用於提供身份驗證憑據(Authorization、Token 授權等)、控制快取或獲取有關使用者代理(user agent)或引薦來源網址(referrer)等的資訊。
- 響應標頭:包含有關響應的額外資訊,例如響應的位置(Location)、響應時間(Date)、最後更新時間(Last-Modified)或者關於伺服器自身的資訊(Server,包括名字、版本等)。
- 表示標頭:包含訊息主體中資源的元資料(例如,編碼、MIME 媒體型別、壓縮方案等)。包括 Content-Type、Content-Encoding、Content-Language 和 Content-Location。
- 有效負荷標頭:包含有關有效載荷資料表示的單獨資訊,包括內容長度和用於傳輸的編碼。包括 Content-Length、Content-Range、Trailer 和 Transfer-Encoding。
- 並非所有可以出現在請求中的標頭都被規範稱為請求標頭,並非所有出現在響應中的標頭都根據規範將其歸類為響應標頭。例如,Content-Type 就是一個表示標頭,在請求中 (如 POST 或 PUT),客戶端告訴伺服器實際傳送的資料型別;在響應中,Content-Type 標頭告訴客戶端實際返回的內容的內容型別。
- 下圖列出了一些與請求和響應相關常見的標頭。
響應狀態碼
用來表明特定 HTTP 請求是否成功完成,歸為以下五大類: 1. 資訊響應 (100–199) 2. 成功響應 (200–299) 3. 重定向訊息 (300–399) 4. 客戶端錯誤響應 (400–499) 5. 服務端錯誤響應 (500–599)
Cookie
Cookie 是伺服器傳送到使用者瀏覽器並儲存在本地的一小塊資料。HTTP 是無狀態協議,這意味著伺服器不會在兩個請求之間保留任何資料(狀態),而 Cookie 使基於無狀態的 HTTP 協議記錄穩定的狀態資訊成為了可能。瀏覽器會儲存 Cookie 並在下次向同一伺服器再發起請求時攜帶併發送到伺服器上。
工作機制如下:
伺服器使用Set-Cookie 響應標頭向用戶代理(一般是瀏覽器)傳送 Cookie 資訊。例如:
Set-Cookie: <cookie-name>=<cookie-value>
這指示伺服器傳送標頭告知客戶端儲存一對 Cookie:
```
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[頁面內容]
現在,對該伺服器發起的每一次新請求,瀏覽器都會將之前儲存的 Cookie 資訊通過**Cookie 請求標頭**再發送給伺服器。
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
Secure 屬性和 HttpOnly 屬性可以確保 Cookie 被安全傳送,並且不會被意外的參與者或指令碼訪問。
1. 標記為 Secure 的 Cookie 只應通過被 HTTPS 協議加密過的請求傳送給服務端。它永遠不會使用不安全的 HTTP 傳送(本地主機除外),這意味著中間人攻擊者無法輕鬆訪問它。不安全的站點(http)無法使用 Secure 屬性設定 Cookie。但是,Secure 不會阻止對 Cookie 中敏感資訊的訪問。例如,有權訪問客戶端硬碟(如果未設定 HttpOnly 屬性,則有權訪問 JavaScript)的人可以讀取和修改它。
2. JavaScript Document.cookie 無法訪問帶有 HttpOnly 屬性的 Cookie,此類 Cookie 僅作用於伺服器。例如,持久化伺服器端會話的 Cookie 不需要對 JavaScript 可用,而應具有 HttpOnly 屬性。此預防措施有助於緩解跨站點指令碼(XSS)攻擊。
Cookie 主要用於以下三個方面: - 會話狀態管理 - 如使用者登入狀態、購物車、遊戲分數或其它需要記錄的資訊 - 個性化設定 - 如使用者自定義設定、主題和其他設定 - 瀏覽器行為跟蹤 - 如跟蹤分析使用者行為等
Cookie 曾一度用於客戶端資料的儲存,因當時並沒有其它合適的儲存辦法而作為唯一的儲存手段,但現在推薦使用現代儲存 API。新的瀏覽器 API 已經允許開發者直接將資料儲存到本地,如使用 Web storage API(localStorage 和 sessionStorage)或 IndexedDB。
Cookie vs Session vs Token
由於伺服器指定 Cookie 後,瀏覽器的每次請求都會攜帶 Cookie 資料,會帶來額外的效能開銷(尤其是在移動環境下)。
以購物車為例,需要有一個機制記錄每個連線的關係,這樣我們就知道加入購物車的商品到底屬於誰了,每次瀏覽器請求後 server 都會將本次商品 id 儲存在 Cookie 中返回給客戶端,客戶端會將 Cookie 儲存在本地,下一次再將上次儲存在本地的 Cookie 傳給 server,這樣每個 Cookie 都儲存著使用者資訊和商品 id。
但是,隨著購物車內的商品越來越多,每次請求的 cookie 也越來越大,這對每個請求來說是一個很大的負擔,我只是想將一個商品加入購買車,為何要將歷史的商品記錄也一起返回給 server?而且,購物車資訊其實已經儲存在 server 中了。
Session
由於使用者的購物車資訊都會儲存在 Server 中,所以在 Cookie 裡只要儲存能識別使用者身份的資訊,知道是誰發起了加入購物車操作即可,這樣每次請求後只要在 Cookie 裡帶上使用者的身份資訊,請求體裡也只要帶上本次加入購物車的商品 id,大大減少了 cookie 的體積大小,我們把這種能識別哪個請求由哪個使用者發起的機制稱為 Session(會話機制),生成的能識別使用者身份資訊的字串稱為 sessionId。
- 首先使用者登入,server 會為使用者生成一個 session,為其分配唯一的 sessionId,這個 sessionId 是與某個使用者繫結的,也就是說根據此 sessionid(假設為 abc) 可以查詢到它到底是哪個使用者,然後將此 sessionid 通過 cookie 傳給瀏覽器。
- 之後瀏覽器的每次新增購物車請求中只要在 cookie 裡帶上 sessionId=abc 這一個鍵值對即可,server 根據 sessionId 找到它對應的使用者後,把傳過來的商品 id 儲存到 server 中對應使用者的購物車即可。
可以看到通過這種方式再也不需要在 cookie 裡傳所有的購物車的商品 id 了,大大減輕了請求的負擔!另外,cookie 是儲存在 client 的,而 session 儲存在 server,sessionId 需要藉助 cookie 的傳遞才有意義。
但是,上述情況能正常工作是因為我們假設 server 是單機工作的,實際生產中,為了保障高可用,一般伺服器至少需要兩臺機器,客戶端請求後,由負載均衡器(如 Nginx)來決定到底打到哪臺機器。
假設登入請求打到了 A 機器,A 機器生成了 session 並在 cookie 裡新增 sessionId 返回給了瀏覽器,那麼問題來了:下次新增購物車時如果請求打到了 B 或者 C,由於 session 是在 A 機器生成的,此時的 B,C 是找不到 session 的,那麼就會發生無法新增購物車的錯誤,就得重新登入了。
目前各大公司普遍採用的方案是將 session 儲存在 redis,memcached 等中介軟體中,請求到來時,各個機器去這些中介軟體取一下 session 即可。就是每個請求都要去 redis 取一下 session,多了一次內部連線,消耗了一點效能,另外為了保證 redis 的高可用,必須做叢集,當然了對於大公司來說,redis 叢集基本都會部署,所以這方案可以說是大公司的首選了。
但是,對於小廠來說可能它的業務量還未達到用 redis 的程度,那有沒有其他不用 server 儲存 session 的使用者身份校驗機制呢?
Token
token(JSON Web Token,JWT)主要由三部分組成: - header:指定了簽名演算法。 - payload:可以指定使用者 id,過期時間等非敏感資料。 - Signature: 簽名,server 根據 header 知道它該用哪種簽名演算法,再用金鑰根據此簽名演算法對 head + payload 生成簽名,這樣一個 token 就生成了。
其中,header, payload 是以 base64 的形式存在的。 1. 首先請求方輸入自己的使用者名稱,密碼,然後 server 據此生成 token,客戶端拿到 token 後會儲存到本地(服務端沒有儲存),之後向 server 請求時在請求頭帶上此 token 即可。 2. 當 server 收到瀏覽器傳過來的 token 時,它會首先取出 token 中的 header + payload,根據金鑰生成簽名,然後再與 token 中的簽名比對,如果成功則說明簽名是合法的,即 token 是合法的。 3. 只要 server 保證金鑰不洩露,那麼生成的 token 就是安全的,因為如果偽造 token 的話在簽名驗證環節是無法通過的。 4. 鑑權 - session 會根據 sessionId 找到 userid 呢,token 如何知道是哪個使用者? - token 中的 payload 中存有我們的 userId,所以拿到 token 後直接在 payload 中就可獲取 userid,避免了像 session 那樣要從 redis 去取的開銷。
可以看到通過這種方式有效地避免了 token 必須儲存在 server 的弊端,實現了分散式儲存。
注意
- token 一旦由 server 生成,它就是有效的,直到過期,無法讓 token 失效,除非在 server 為 token 設立一個黑名單,在校驗 token 前先過一遍此黑名單,如果在黑名單裡則此 token 失效,但一旦這樣做的話,那就意味著黑名單就必須儲存在 server,這又回到了 session 的模式,那直接用 session 不香嗎。所以一般的做法是當客戶端登出要讓 token 失效時,直接在本地移除 token 即可,下次登入重新生成 token 就好。
- token 是存在瀏覽器的,如果放在 cookie 裡可能導致 cookie 超限(cookie 一般有大小限制的,如 4kb),那就只好放在 local storage 裡,但這樣會造成安全隱患,因為 local storage 這類的本地儲存是可以被 JS 直接讀取的,另外上文也提到,token 一旦生成無法讓其失效,必須等到其過期才行,這樣的話如果服務端檢測到了一個安全威脅,也無法使相關的 token 失效。所以 token 更適合一次性的命令認證,設定一個比較短的有效期。
SSO
Cookie 跨站是不能共享的,所以使用 Cookie 實現多應用(多系統)的單點登入(SSO,指在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統)的話就很困難(要用比較複雜的 trick 來實現)。
傳送 token 時一般放在標頭的 Authorization 自定義頭裡,不是放在 Cookie 裡的,這主要是因為跨域不能共享 Cookie。如果用 token 來實現 SSO 會非常簡單,只要在標頭中的 authorize 欄位(或其他自定義)加上 token 即可完成所有跨域站點的認證。
跨站請求偽造(CSRF)
是一種冒充受信任使用者,向伺服器傳送非預期請求的攻擊方式。比如使用者登入了某銀行網站(假設為 http://www.examplebank.com/
,並且轉賬地址為 http://www.examplebank.com/withdraw?amount=1000&transferTo=PayeeName
),登入銀行網站後 cookie 裡會包含登入使用者的 sessionid,攻擊者可以在另一個網站上放置如下程式碼
html
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
那麼如果正常的使用者誤點了上面這張圖片,由於相同域名的請求會自動帶上 cookie,而 cookie 裡帶有正常登入使用者的 sessionid 等身份認證的資訊,銀行網站會認為是真正的使用者操作而去執行,上面這樣的轉賬操作在 server 就會成功,會造成極大的安全風險。
CSRF 攻擊的根本原因在於對於同樣域名的每個請求來說,它的 cookie 都會被自動帶上,這個是瀏覽器的機制決定的,所以很多人據此認定 cookie 不安全。
使用 token 確實避免了 CSRF 的問題,但正如上文所述,由於 token 儲存在 local storage,它會被 JS 讀取,從儲存角度來看也不安全(實際上防護 CSRF 攻擊的正確方式是用 CSRF token)。
所以不管是 cookie 還是 token,從儲存角度來看其實都不安全,都有暴露的風險,我們所說的安全更多的是強調傳輸中的安全,可以用 HTTPS 協議來傳輸, 這樣的話請求頭都能被加密,也就保證了傳輸中的安全。
總結
- 其實我們把 cookie 和 token 比較本身就不合理,一個是儲存方式,一個是驗證方式,正確的比較應該是 session vs token。
- session 和 token 本質上是沒有區別的,都是對使用者身份的認證機制,只是他們實現的校驗機制不一樣而已(一個儲存在 server,通過在 redis 等中介軟體獲取來校驗,一個儲存在 client,通過簽名校驗的方式來校驗),多數場景上使用 session 會更合理,但如果在單點登入,一次性命令認證上使用 token 會更合適,最好在不同的業務場景中合理選型,才能達到事半功倍的效果。
RESTful API
REST(表現層狀態轉換,英語:Representational State Transfer)是一種設計提供全球資訊網絡服務的軟體構建風格,目的是便於不同軟體/程式在網路(例如網際網路)中互相傳遞資訊。
它基於超文字傳輸協議(HTTP)之上而確定的一組約束和屬性,RESTful 就代表滿足 REST 原則的。 1. 每一個 URI 代表一種資源。 2. 客戶端和伺服器之間,傳遞這種資源的某種表現層。 3. 客戶端通過 HTTP 方法,對伺服器資源進行操作,實現“表現層狀態轉化”。
跨源資源共享(CORS)
- 同源策略是瀏覽器的一個重要的安全策略(不屬於 HTTP),它用於限制一個源(Origin)的文件或者它載入的指令碼如何能與另一個源的資源進行互動。它能幫助阻隔惡意文件,減少可能被攻擊的媒介。
- 如果兩個 URL 的協議、埠(如果指定)和主機都相同,則兩個 URL 是同源。
- 瀏覽器某些操作僅限於同源內容,但可以使用 CORS 解除這個限制。
- CORS 是一種基於 HTTP 標頭的機制,通過這些 HTTP 標頭決定瀏覽器是否阻止 JavaScript 程式碼獲取跨源請求的響應。即 CORS 給了 web 伺服器這樣的許可權:伺服器可以選擇是否允許跨域請求訪問到它的資源。
- 另外幾個解決跨域問題的方法:
- 代理伺服器 - 通過部署一個與當前域名同源的伺服器,請求時發到代理伺服器,再由代理伺服器轉發到真實伺服器;然後真實伺服器響應給代理伺服器,代理伺服器再把響應轉發到我們的瀏覽器上。例如 Webpack 的一個外掛 devServer 就具備了代理伺服器的功能,可以在開發模式下幫助我們進行聯調。
- iframe - 通過把一個 src 與伺服器同源的 iframe 元素嵌入到頁面中,再通過 window.postMessage 來實現 iframe 元素與當前頁面通訊。
- JSONP - 嵌入的跨域資源不受同源策略約束。利用這個開放策略,使用 script 標籤替代 XMLHttpRequest 物件或 fetch 來請求資料。用 JSONP 抓到的資料並不是 JSON,而是任意的 JavaScript 程式碼。
HTTPS
- HTTPS(超文字傳輸安全協議,英語:HyperText Transfer Protocol Secure)是 HTTP 協議的加密版本。
- 它使用 SSL 或 TLS 協議來加密客戶端和伺服器之間所有的通訊。
- 安全連線允許客戶端與伺服器安全地交換敏感資料,例如網上銀行或者線上商城等涉及金錢的操作。
- 對稱加密
- 加密方和解密使用同一金鑰
- 加密解密的速度比較快
- 常見有:Blowfish、IDEA、RC5、RC6、DES、3DES、AES。
- 非對稱加密
- 使用兩把金鑰進行加密和解密,即公鑰(public key)和私鑰(private key)
- 公鑰加密私鑰解密,私鑰加密公鑰可以解密
- 加密或者解密,速度非常慢
- 私鑰和公鑰是成對出現的
- 常見有:RSA、DSA、Elgamal、揹包演算法、Rabin、D-H、ECC。
- HTTPS 同時使用了對稱加密(效能)和非對稱加密(安全),並使用 CA 機構頒發的數字證書解決公鑰傳輸問題。
如何使用 HTTPS
以訪問 www.helloworld.net
網站為例,分為 3 個階段:
1. 網站申請證書階段:
1. 網站向 CA 機構申請數字證書(需要提交一些材料,比如域名)。
2. CA 向證書中寫入摘要演算法,域名,網站的公鑰等重要資訊。
3. CA 根據證書中寫入的摘要演算法,計算出證書的摘要。
4. CA 用自己的私鑰對摘要進行加密,計算出簽名。
5. CA 生成一張數字證書,頒發給了 www.helloworld.net
。
6. 網站的管理員,把證書放在自己的伺服器上。
-
瀏覽器驗證證書階段:
- 瀏覽器在位址列中輸入
https://www.helloworld.net
並回車。 - 伺服器將數字證書傳送給瀏覽器。
- 瀏覽器用作業系統內建的 CA 的數字證書,拿到 CA 的公鑰。
- 瀏覽器用 CA 公鑰對
www.helloworld.net
的數字證書進行驗籤。 - 具體就是,瀏覽器用 CA 公鑰,對 helloworld 的數字證書中的簽名進行解密,得到摘要 D1。
- 瀏覽器根據 helloworld 數字證書中的摘要演算法,計算出證書的摘要 D2。
- 對比 D1 和 D2 是否相等。
- 如果不相等,說明證書被掉包了。
- 如果相等,說明證書驗證通過了。
- 瀏覽器在位址列中輸入
-
協商對稱加密金鑰階段:
- 瀏覽器驗證數字證書通過以後。
- 瀏覽器拿到數字證書中的公鑰,也就是
www.helloworld.net
網站的公鑰。 - 瀏覽器有了網站的公鑰後,就用公鑰進行對金鑰S進行加密,加密以後的密文傳送給伺服器。
- 伺服器收到密文後,用自己的私鑰進行解密,得到金鑰S。
- 此後瀏覽器,伺服器雙方就用金鑰S進行對稱加密的通訊了。