文章目錄
一、HTTP簡介
HTTP簡介在前篇博客:Web三大技術要素中有介紹,三大技術要素之間相互關聯依賴,比如HTML中的超鏈接使用URL,HTTP報文中的主體部分則是HTML文檔或其內包含的MIME資源。
本文主要介紹HTTP協議部分,由於目前使用最廣泛的仍然是HTTP/1.1 版本,截至2020年4月,W3Techs統計的前1000萬網站中支持HTTP/2的大概佔43.6%,支持QUIC的僅佔4.2%。HTTP/2也是從HTTP/1.1演化升級而來,要了解HTTP/2同樣離不開HTTP/1.1,因此本文先介紹主流的HTTP/1.1版本。
我們再看看HTTP協議在整個網絡分層參考模型中的位置,HTTP屬於應用層協議,底層依賴於TCP/IP協議完成數據報文的傳輸。如果對HTTP報文有加密傳輸的需求,在HTTP協議與TCP協議之間還可以增加SSL/TLS協議層,這就構成了HTTPS協議。
在介紹HTTP/1.1 報文結構前,再回顧下HTTP/1.1 解決了前代的哪些問題或者帶來了哪些性能提升:
HTTP/1.0問題 | HTTP/1.1改進 |
---|---|
不能讓多個請求共用一個連接 | 默認支持持久連接(keep-alive),管道化(pipelining)特性允許客戶端在一次TCP連接中發送所有的請求,這對於提升性能和效率而言意義重大 |
缺少強制的 Host 首部 | 強制要求客戶端提供 Host 首部,讓虛擬主機託管(在一個 IP 上提供多個 Web 服務主機域名)成爲可能 |
緩存控制相當簡陋 | 擴展緩存相關首部以增強緩存控制,增加Range請求以支持斷點續傳,增加Upgrade 首部以支持升級到其它協議比如WebSocket |
僅支持基本的GET、POST 和 HEAD請求方法 | 增加了OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 等六種請求方法,擴展了功能 |
上面提到的最重要性能改進 — HTTP持久連接與管道化機制在前篇博客Web三大技術要素中也介紹過,這裏再給出圖示:
TCP三次握手與四次揮手的過程比較耗時,現在每個網頁的平均資源數接近兩百,如果每個資源請求都執行一次TCP連接/斷開,對網絡資源的浪費相當大。因此,HTTP/1.1 支持多個請求共用一次TCP連接,也即在請求網頁時執行一次TCP連接,隨後客戶端向服務器發送該網頁相關的所有資源請求,既節省了大量重複的TCP連接/斷開開銷,又不需要等收到資源響應後再發送下一個資源請求,極大提升了Web性能與效率。
HTTP/1.1 雖然支持以流水線的形式在一次TCP連接中發送指定網頁的所有資源請求,但是服務器仍然只能按順序響應請求,如果處理某個請求花了很長時間,那麼隊頭阻塞(head of line blocking)會影響其他請求,這是HTTP/1.1 待改進的一個方面。
HTTP/1.1 誕生於1997年,到現在已經二十多年過去了,Web發展更是突飛猛進,HTTP/1.1 的性能瓶頸越來越多,比如每次資源請求都需要發送越來越大的的報文首部,每個網頁中平均近兩百個資源的報文首部大同小異,這又是對網絡資源的極大浪費。隨着對Web性能的需求越來越高,針對HTTP/1.1 的性能瓶頸出現了五花八門的優化方案,直到近20年後的2015年,HTTP/2才姍姍來遲,解決了HTTP/1.1 遺留的諸多瓶頸,跟上了目前跨平臺多終端Web的性能需求。
二、HTTP報文
HTTP協議採用Client / Browser — Server 架構通信,當我們使用Client / Browser 訪問Web時,在瀏覽器地址欄輸入想獲取資源頁面的URL,Browser 向Server 發送 HTTP請求報文,告訴服務器想請求哪個資源,Server 找到或生成請求的資源後,通過HTTP響應報文將資源發送給Browser / Client,瀏覽器或客戶端接收到HTML資源頁面後,將其處理成更直觀的形式展示給你,整個過程圖示如下:
2.1 HTTP Request
HTTP 請求報文包括請求行、請求頭部、空行、報文主體等部分構成,請求行包括請求方法、URL、協議版本三部分,請求首部主要包括請求的各種條件或屬性,空行是爲了分割首部與主體,報文主體則包含應被髮送的數據。HTTP請求報文結構與示例如下:
HTTP 請求方法是客戶端用來告知服務器自己意圖的,HTTP/1.1支持的請求方法及功能描述如下表示(請求方法名要使用大寫字母):
方法 | 描述 |
---|---|
GET | 用來請求訪問已被 URL 指定的資源,請求條件或參數被包含在URL中,指定的資源經服務器端解析後返回響應內容,操作是安全且冪等的;如果請求的資源是靜態文本則保持原樣返回,如果是像 CGI / Servlet 那樣的程序則返回經過執行後的輸出結果 |
HEAD | 和 GET 方法一樣,只是不返回報文主體部分,常用於確認URL 的有效性及資源更新的日期時間等 |
POST | 向指定的資源提交數據進行處理的請求,比如提交表單或上傳文件,數據被包含在請求報文主體中,操作不是冪等的 |
PUT | 從客戶端向服務器傳送數據取代 URL 指定的資源內容,操作是冪等的(不管進行多少次相同的操作,結果都一樣) |
PATCH | 是對 PUT 方法的補充,用來對已知資源進行局部更新 |
DELETE | 請求服務器刪除 URL 指定的資源,操作是冪等的 |
OPTIONS | 用來查詢服務器針對指定URL 支持的 HTTP 方法 |
TRACE | 讓客戶端可以看到中間服務器進行了哪些更改或添加,主要用於測試或診斷 |
CONNECT | 將請求連接轉換爲透明的TCP/IP 隧道,通常是爲了通過未加密的 HTTP 代理促進TLS/SSL 加密通信 (HTTPS) |
HTTP/1.1 請求方法中比較基本的有四種:GET、POST、PUT、DELETE(HEAD與PATCH方法可以分別看作GET與PUT方法的補充),可以分別對應訪問資源的四種基本操作:查、增、改、刪。符合REST(Representational State Transfer,表現層狀態轉化)互聯網軟件架構原則的RESTful API 正式通過HTTP的這四種基本請求方法完成對資源或數據對象的增、刪、改、查等操作的,關於RESTful 架構設計會在下文給出簡單介紹。
在辨析HTTP的四種基本請求方法之前,需要先解釋下上表提到的兩個概念 — 安全和冪等:
- 安全性:對資源的操作不會影響該資源的狀態,也就是說,它僅僅是獲取資源信息,就像數據庫查詢一樣,不會修改、增加、刪除數據;
- 冪等性:原本是數學上的概念 — 使公式:f(x)=f(f(x)) 能夠成立的數學性質。用在編程領域,則意爲對同一個系統,使用同樣的條件,一次請求和重複的多次請求對系統資源狀態的影響是一致的。
HTTP提供的四種基本請求方法中,只有GET方法要求是安全的,POST、PUT、DELETE方法都不是安全的;GET、PUT、DELETE三個方法都是冪等的,只有POST方法不是冪等的。爲什麼專門區分冪等性呢?舉個例子,我們購買商品後支付,支付扣款成功,但是返回結果的時候網絡異常,此時錢已經扣了,我們再次點擊付款按鈕,如果付款請求不是冪等的,那麼會進行第二次扣款,如果付款請求是冪等的,多次相同的付款請求跟一次付款請求的結果是一樣的,不會進行多次重複扣款。
方法 | 是否安全 | 是否冪等 | RESTful API 操作對應HTTP請求方法 |
---|---|---|---|
GET | 是 | 是 | 查詢操作 |
POST | 否 | 否 | 新增操作 |
PUT | 否 | 是 | 更新操作 |
DELETE | 否 | 是 | 刪除操作 |
通過安全性與冪等性兩個概念,應該可以清晰區分HTTP四種基本方法的使用了,比較容易混淆的是POST方法與PUT方法,這兩個方法的本質區別在於是否滿足冪等性,POST方法還可以提交URL 所指定資源的子資源(從屬資源),比如分別用POST與PUT方法創建資源,POST方法可以只指定集合資源URL(也即只指定父資源URL,待創建子資源的名字可以由服務器分配),PUT方法則必須指定具體資源的URL(待創建子資源的名字需要客戶端在URL中指定),很多時候我們要創建的子資源命名交由服務器分配更高效省事(比如可以避免URL重複等),所以在RESTful API 中常使用POST方法來創建資源;PUT方法本就是冪等性替換整個目標資源,因此在RESTful API 中常用PUT方法來更新資源(POST方法不是冪等操作,因此不適合用來更新資源)。
我們訪問Web時,最常用的方法是GET和POST,GET方法常用來從服務器獲取目標網頁資源信息,POST方法常用來向服務器提交資源數據(比如創建新博客、發表新狀態或者新評論、上傳新文件等)。HTTP/1.1 的這些請求方法自身不帶驗證機制,存在安全性問題,一般需要配合Web應用程序的驗證機制(比如註冊並登錄自己的賬號),用戶只能對部分擁有相應權限的資源使用PUT或DELETE方法(比如用戶對自己創建的博客或評論才擁有更新或刪除的權限)。
請求報文頭部字段比較多,下文再詳細介紹,請求報文主體一般使用HTML語法來描述資源信息,HTML語法在前篇博客:Web簡史與三大技術要素中有過簡介,這裏也不再贅述了。
2.2 HTTP Response
HTTP響應報文包括狀態行、響應頭部、空行、報文主體等部分構成,響應行包括協議版本、狀態碼、狀態原因短語三部分,響應頭部主要包括響應的各種條件或屬性,空行用於分割首部和主體,報文主體則包含應被髮送的數據。HTTP響應報文結構與示例如下:
HTTP狀態碼由三個十進制數字組成,第一個十進制數字定義了狀態碼的類型,後兩個數字沒有分類的作用,HTTP狀態碼共分爲5種類型:
狀態碼 | 類別 | 狀態原因短語 | 常見狀態碼 |
---|---|---|---|
1XX | Informational(信息性狀態碼) | 服務器接收到請求,需要請求者繼續執行操作 | 100、101 |
2XX | Success(成功狀態碼) | 請求已被正確處理完畢 | 200、204、206 |
3XX | Redirection(重定向狀態碼) | 資源位置已經變動,需要進行附加操作以完成請求 | 301、302、304 |
4XX | Client Error(客戶端錯誤狀態碼) | 請求報文包含語法錯誤,服務器無法處理請求 | 400、401、403、404 |
5XX | Server Error(服務器錯誤狀態碼) | 服務器處理請求時出錯 | 500、501、502、503 |
比較常見的HTTP響應狀態碼及其描述舉例如下(想了解更多響應碼含義可參考:HTTP狀態碼):
狀態碼 | 狀態名 | 描述 |
---|---|---|
100 | Continue | 客戶端應繼續其請求 |
101 | Switching Protocols | 服務器根據客戶端的請求切換協議,只能切換到更高級的協議,例如切換到HTTP的新版本協議 |
200 | OK | 從客戶端發來的請求在服務器端被成功處理了 |
204 | No Content | 與 200 OK 基本相同,但在返回的響應報文中不含報文主體 body 部分 |
206 | Partial Content | 也是服務器處理成功的狀態,表示響應返回的 body 數據並不是資源的全部,而是其中的一部分。響應報文中包含由 Content-Range 指定範圍的實體內容,常用於 HTTP 分塊下載或斷電續傳 |
301 | Moved Permanently | 表示永久重定向,說明請求的資源已經不存在了,需改用新的 URL 再次訪問。在響應頭裏使用字段 Location,指明後續要跳轉的 URL,瀏覽器會自動重定向新的 URL |
302 | Found | 表示臨時重定向,說明請求的資源還在,但暫時需要用另一個 URL 來訪問,跟 301 一樣使用Location 字段指明後續要跳轉的URL |
304 | Not Modified | 所請求的資源未修改,重定向已存在的緩衝文件,也稱緩存重定向,用於緩存控制。客戶端通常會緩存訪問過的資源,通過提供一個頭信息指出客戶端希望只返回在指定日期之後修改的資源 |
400 | Bad Request | 客戶端請求的語法錯誤,服務器無法理解 |
401 | Unauthorized | 發送的請求需要有通過 HTTP 認證(BASIC 認證、DIGEST 認證)的認證信息 |
403 | Forbidden | 服務器理解請求客戶端的請求,但是拒絕執行此請求 |
404 | Not Found | 請求的資源在服務器上不存在或未找到,所以無法提供給客戶端 |
500 | Internal Server Error | 服務器內部錯誤,無法完成請求 |
501 | Not Implemented | 服務器不支持請求的功能,無法完成請求 |
502 | Bad Gateway | 通常是服務器作爲網關或代理時返回的錯誤碼,表示服務器自身工作正常,訪問後端服務器發生了錯誤 |
503 | Service Unavailable | 由於超載或系統維護,服務器暫時無法處理客戶端的請求,延時的長度可包含在服務器的Retry-After頭信息中 |
響應報文頭部字段也在下文跟請求報文頭部字段放到一起介紹,這裏先暫略了,響應報文主體也是使用HTML語法來描述資源信息的。HTTP/1.1 報文主體是可以被壓縮傳送的,也支持分塊傳輸,但報文首部不能被壓縮。
2.3 HTTP Header Fields
HTTP 首部字段是構成 HTTP 報文的要素之一,在客戶端與服務器之間以 HTTP 協議進行通信的過程中,無論是請求還是響應都會使用首部字段,它能起到傳遞額外重要信息的作用,比如提供報文主體大小、所使用的語言、認證信息等內容。從前面給出的HTTP報文格式圖示可以看出,HTTP 首部字段是由首部字段名和字段值構成的,中間用冒號“:” 分隔。
HTTP 首部字段根據實際用途被分爲以下 4 種類型:
- 通用首部字段(General Header Fields):請求報文和響應報文兩方都會使用的首部;
- 請求首部字段(Request Header Fields):從客戶端向服務器端發送請求報文時使用的首部,補充了請求的附加內容、客戶端信息、響應內容相關優先級等信息;
- 響應首部字段(Response Header Fields):從服務器端向客戶端返回響應報文時使用的首部,補充了響應的附加內容,也會要求客戶端附加額外的內容信息;
- 實體首部字段(Entity Header Fields):針對請求報文和響應報文的實體部分使用的首部,補充了資源內容更新時間等與實體有關的信息。
2.3.1 General Header Fields
HTTP/1.1 規範RFC2616中定義的通用首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Cache-Control | 控制緩存的行爲 | Cache-Control: private, max-age=0, no-cache |
Connection | 逐跳首部、連接的管理 | Connection: Keep-Alive |
Date | 創建報文的日期時間 | Date: Tue, 03 Jul 2020 04:40:59 GMT |
Pragma | 爲與 HTTP/1.0兼容而定義的指令字段 | Pragma: no-cache |
Trailer | 事先說明在報文主體後記錄了哪些首部字段 | Trailer: Expires |
Transfer-Encoding | 指定報文主體的傳輸編碼方式,支持分塊傳輸 | Transfer-Encoding: chunked |
Upgrade | 升級爲其他協議, 常與Connection字段一起使用 |
Upgrade: TLS/1.0 Connection:Upgrade |
Via | 追蹤請求和響應報文的傳輸路徑, 常與TRACE 方法一起使用 |
Via: 1.0 gw.hackr.jp (Squid/3.1) |
Warning | 會告知用戶一些與緩存相關的問題的警告 | Warning: [警告碼][警告的主機:端口號]“[警告內容]”([日期時間]) |
這裏重點介紹下緩存控制字段Cache-Control,緩存是指代理服務器或客戶端本地磁盤內保存的資源副本。利用緩存可減少對源服務器的訪問,因此也就節省了通信流量和通信時間。但有些資源不能緩存(比如涉及安全隱私的敏感信息)、有些不常訪問的資源沒必要緩存、有些經常訪問的資源需要緩存、有些緩存資源已過期需要更新等,這些都可以靠首部字段Cache-Control來控制緩存的行爲。緩存字段值可以配置多個指令,多指令間通過逗號分隔,用於請求和響應報文中的字段值或指令有一定差異,可用的緩存請求 / 響應指令對比如下:
先辨析兩個容易混淆的指令:
- no-cache:不緩存過期的資源,緩存會向源服務器進行有效期確認後處理資源;
- no-store:不緩存請求或響應的任何內容,請求或響應中可能包含機密或敏感信息。
即便緩存服務器(一種代理服務器)或客戶端瀏覽器內有緩存,也不能保證每次都會返回對相同資源的請求,因爲這關係到被緩存資源的有效性問題。當遇上源服務器上的資源更新時,如果還是使用不變的緩存,那就會演變成返回更新前的“舊”資源了。即使存在緩存,也會因爲客戶端的要求、緩存的有效期等因素,向源服務器確認資源的有效性。若判斷緩存有效,則可直接從緩存服務器或客戶端本地磁盤內讀取資源數據;若判斷緩存失效,緩存服務器將會再次從源服務器上獲取“新”資源。
Connection 首部字段具備如下兩個作用:控制不再轉發給代理的首部字段;管理持久連接(配置值爲Keep-Alive,前面已經介紹過了)。在客戶端發送請求和服務器返回響應內,使用 Connection 首部字段,可以控制代理不再轉發該首部字段,比如Connection:Upgrade表示經過代理時先刪除掉Upgrade 首部字段再轉發該報文。這就要求需要升級通信協議(比如Upgrade: WebSocket 或Upgrade: HTTP/2)的客戶端與服務器之間不能有代理服務器,二者應該是直接連接。對於附有首部字段 Upgrade 的請求,服務器可用 101 Switching Protocols 狀態碼作爲響應返回。
2.3.2 Request Header Fields
HTTP/1.1 規範RFC2616中定義的請求首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Accept | 用戶代理可處理的媒體類型 | Accept:text/html,application/xhtml+xml,application/xml;q=0.9 |
Accept-Charset | 可處理的字符集及其優先級 | Accept-Charset: iso-8859-5,unicode-1-1;q=0.8 |
Accept-Encoding | 可處理的內容編碼及其優先級 | Accept-Encoding: gzip,compress,deflate |
Accept-Language | 可處理的自然語言及其優先級 | Accept-Language: zh-cn,zh;q=0.7,en-us,en;q=0.3 |
Authorization | Web服務器要求客戶端的認證信息 | Authorization: Basic dWVub3NlbjpwYXNzd29yZA== |
Expect | 期待服務器的特定行爲 | Expect: 100-continue |
From | 用戶的電子郵箱地址 | From: [email protected] |
Host | 請求資源所在服務器主機名或域名 | Host: www.baidu.com |
If-Match | 比較實體標記(ETag) | If-Match: “123456” |
If-Modified-Since | 比較資源的更新時間 | If-Modified-Since: Thu, 15 Apr 2018 00:00:00 GMT |
If-None-Match | 比較實體標記(與 If-Match 相反) | If-None-Match: * |
If-Range | 資源未更新時發送實體 Byte 的範圍請求 | If-Range: “123456” |
If-Unmodified-Since | 比較資源的更新時間 (與If-Modified-Since相反) |
If-Unmodified-Since: Thu, 03 Jul 2018 00:00:00 GMT |
Max-Forwards | 最大傳輸逐跳數 | Max-Forwards: 10 |
Proxy-Authorization | 代理服務器要求客戶端的認證信息 | Proxy-Authorization: Basic dGlwOjkpNLAGfFY5 |
Range | 實體的字節範圍請求 | Range: bytes=5001-10000 |
Referer | 對請求中 URI 的原始獲取方 | Referer: http://www.baidu.com/index.html |
TE | 傳輸編碼格式及其優先級 | TE: gzip,deflate;q=0.5 |
User-Agent | HTTP 客戶端程序的信息 | user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/81.0.4044.92 |
首部字段 Accept 是客戶端告訴服務器自己能處理的媒體類型(比如文本文件、圖片文件、音視頻文件、二進制文件等MIME類型)和相應的優先級(使用q 表示優先級權重值,用分號與對應的媒體類型分割,權重值q 範圍是0~1,不指定權重值q 則默認爲 q = 1.0)。接下來的Accept-Charset字段、Accept-Encoding字段、Accept-Language分別告訴服務器自己能處理的字符集類型、編碼類型、語言類型及其對應的優先級或權重值。
首部字段 Host 用來配置服務器主機名(如果URL 中已經是一個絕對路徑可以唯一定位目標資源,則會忽略Host字段),藉助Host字段可以在一個IP(也即一個物理主機)上配置多個域名(也即多個Web服務),實現虛擬主機託管功能。
形如 If-xxx 這種樣式的請求首部字段,都可稱爲條件請求。服務器接收到附帶條件的請求後,只有判斷指定條件爲真時,纔會執行請求。
首部字段 Range 可以指定範圍發送,也即只請求自己需要的那部分資源,而不必再次下載整個資源。通過範圍請求,還可以實現斷點續傳功能,因爲某些原因下載中斷,可以通過範圍請求只下載尚未獲得的部分資源,已獲得的資源不必重新下載,進一步提高了網絡利用效率。
首部字段 User-Agent 會將創建請求的瀏覽器和用戶代理名稱等信息傳達給服務器,服務器可以根據這些信息返回適配瀏覽器的頁面,讓同樣的信息可以在不同類型的終端頁面上都有更直觀友好的展示。
2.3.3 Response Header Fields
HTTP/1.1 規範RFC2616中定義的響應首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Accept-Ranges | 是否接受字節範圍請求 | Accept-Ranges: bytes |
Age | 推算資源創建經過的時間 | Age: 600 |
ETag | 資源的唯一標識信息 | ETag: “usagi-1234” |
Location | 令客戶端重定向至指定URI | Location: http://www.baidu.com/index.html |
Proxy-Authenticate | 代理服務器對客戶端的認證信息 | Proxy-Authenticate: Basic realm=“Usagidesign Auth” |
Retry-After | 對再次發起請求的時機要求 | Retry-After: 120 |
Server | HTTP服務器的安裝信息 | Server: Apache/2.2.6 (Unix) PHP/5.2.5 |
Vary | 代理服務器緩存的管理信息 | Vary: Accept-Language |
WWW-Authenticate | 服務器對客戶端的認證信息 | WWW-Authenticate: Basic realm=“Usagidesign Auth” |
首部字段 ETag 是一種可將資源以字符串形式做唯一性標識的方式,服務器會爲每份資源分配對應的 ETag值,當資源更新時,ETag 值也需要更新。通過比較ETag值可以判斷出當前緩存的資源是否有效,前面請求首部字段中的 If-Match / If-None-Match 便是通過比較實體標記 ETag 值來判斷是否執行相應的請求。
首部字段 Location 可以將響應接收方引導至某個與請求 URI 位置不同的資源,該字段常配合 3xx :Redirection 的響應,提供重定向的URI。幾乎所有的瀏覽器在接收到包含首部字段 Location 的響應後,都會強制性地嘗試對已提示的重定向資源的訪問。
首部字段 Retry-After 告知客戶端應該在多久之後再次發送請求。主要配合狀態碼 503 Service Unavailable 響應,或 3xx Redirect 響應一起使用,字段值可以指定爲具體的日期時間或創建響應後的秒數。
某些 Web 頁面只想讓特定的人瀏覽,或者乾脆僅本人可見,爲達到這個目標,必不可少的就是認證功能。認證功能可以讓服務器或代理服務器判斷用戶或客戶端是否有訪問該資源的權限,服務器需要客戶端提供的認證信息通過響應報文中的WWW-Authenticate 字段值傳遞,客戶端向服務器提交的認證信息通過請求報文中的Authorization 字段值提供,BASIC認證步驟如下(代理服務器對客戶端的認證過程與此類似):
2.3.4 Entity Header Fields
HTTP/1.1 規範RFC2616中定義的實體首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Allow | 資源可支持的HTTP方法 | Allow: GET, HEAD |
Content-Encoding | 實體主體適用的編碼方式 | Content-Encoding: gzip |
Content-Language | 實體主體的自然語言 | Content-Language: zh-CN |
Content-Length | 實體主體的大小(單位:字節) | Content-Length: 15000 |
Content-Location | 替代對應資源的URI | Content-Location: http://www.baidu.com/index.html |
Content-MD5 | 實體主體的報文摘要 | Content-MD5: OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY== |
Content-Range | 實體主體的位置範圍 | Content-Range: bytes 5001-10000/10000 |
Content-Type | 實體主體的媒體類型 | Content-Type: text/html; charset=UTF-8 |
Expires | 實體主體過期的日期時間 | Expires: Wed, 04 Jul 2020 08:26:05 GMT |
Last-Modified | 資源的最後修改日期時間 | Last-Modified: Wed, 23 May 2020 09:59:55 GMT |
首部字段 Allow 用於通知客戶端能夠支持 Request-URI 指定資源的所有 HTTP 方法。當服務器接收到不支持的 HTTP 方法時,會以狀態碼405 Method Not Allowed 作爲響應返回。與此同時,還會把所有能支持的 HTTP 方法寫入首部字段 Allow 後返回。
首部字段 Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5分別告知客戶端服務器對實體的主體部分選用的內容編碼(在不丟失實體信息的前提下所進行的壓縮)方式、自然語言、長度 / 字節數(對實體主體進行內容編碼傳輸時,不能再使用 Content-Length首部字段)、相應的URL、MD5摘要字符串 等信息。
首部字段 Content-Range 能告知客戶端作爲響應返回的實體的哪個部分符合範圍請求,字段值以字節爲單位,表示當前發送部分及整個實體大小。
首部字段 Content-Type 說明了實體主體內對象的媒體類型,和請求首部字段 Accept 一樣,字段值用 type/subtype 形式賦值。
2.4 Cookie狀態管理
HTTP 是一種不保存狀態,即無狀態(stateless)協議,它不對之前發生過的請求和響應的狀態進行管理,也即無法根據之前的狀態進行本次的請求處理。這是爲了更快地處理大量事務,確保協議的可伸縮性,而特意把 HTTP 協議設計成如此簡單的。
隨着 Web 的不斷髮展,因無狀態而導致業務處理變得棘手的情況增多了,比如用戶登錄到一家購物網站,即使他跳轉到該站的其它頁面後,也需要能繼續保持登錄狀態,網站爲了能夠掌握是誰送出的請求,需要保存用戶的狀態。HTTP/1.1 雖然是無狀態協議,但爲了實現期望的保持狀態功能,引入了 Cookie 技術,有了 Cookie 再用 HTTP 協議通信,就可以管理狀態了。
Cookie 技術通過在請求和響應報文中寫入 Cookie 信息來控制客戶端的狀態,Cookie 會根據從服務器端發送的響應報文內的一個叫做 Set-Cookie 的首部字段信息,通知客戶端保存 Cookie。當下次客戶端再往該服務器發送請求時,客戶端會自動在請求報文中加入 Cookie 值後發送出去。服務器端發現客戶端發送過來的 Cookie 後,會去檢查究竟是從哪一個客戶端發來的連接請求,然後對比服務器上的記錄,最後得到之前的狀態信息。
管理服務器與客戶端之間狀態的 Cookie,雖然沒有被編入標準化HTTP/1.1 的 RFC2616 中,但在 Web 網站方面得到了廣泛的應用。目前使用最廣泛的 Cookie 標準是在網景公司制定的標準上進行擴展後的產物(可參考RFC6265),下面的表格內列舉了與 Cookie 有關的首部字段:
首部字段名 | 首部類型 | 描述 | 示例 |
---|---|---|---|
Set-Cookie | 響應首部字段 | 開始狀態管理所使用的Cookie信息 | Set-Cookie: sid=1342077140226724; path=/; expires=Wed,10-Oct-12 07:12:20 GMT |
Cookie | 請求首部字段 | 服務器接收到的Cookie信息 | Cookie: sid=1342077140226724 |
響應首部字段Set-Cookie 的值可以配置多種屬性信息,比如Cookie名稱及其值、有效期、適用目錄、適用域名、適用協議等,當客戶端想獲得 HTTP 狀態管理支持時,就會在請求報文中包含從服務器接收到的 Cookie 名稱及其值。
2.5 HTTP報文捕獲分析
本文主要使用兩個工具來捕獲分析HTTP報文:一個是Chrome瀏覽器自帶的Network工具(在開發者工具中);另一個是cURL 命令行工具集。
我們使用curl 工具集(下載地址:https://curl.haxx.se/download.html)訪問百度首頁,請求報文與響應報文內容如下(響應報文主體省略了):
C:\Users\Administrator\Downloads\curl-7.70.0-win64-mingw\bin>curl -v http://www.baidu.com/index.html
* Trying 61.135.169.125:80...
* Connected to www.baidu.com (61.135.169.125) port 80 (#0)
> GET /index.html HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.70.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: keep-alive
< Content-Length: 2381
< Content-Type: text/html
< Date: Fri, 08 May 2020 07:43:39 GMT
< Etag: "588604c4-94d"
< Last-Modified: Mon, 23 Jan 2017 13:27:32 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<
<!DOCTYPE html>
<!--STATUS OK-->
<html>...</html>
* Connection #0 to host www.baidu.com left intact
上面 “> ”開頭的行是客戶端發出的請求,“< ”開頭的行是服務器的應答(行首的“> ”和“< ”本身並不是請求或者應答的一部分,只是curl輸出的一種標記),“* ”開頭的行是HTTP連接建立以前curl輸出的一些診斷信息,比如我們可以看到curl通過DNS查找到“www.baidu.com”對應的IP—— 61.135.169.121。
上面的請求報文使用GET 方法,協議版本爲 HTTP/1.1,百度首頁的URL爲 “http://www.baidu.com/index.html”(首部字段Host 指定服務主機域名),首部字段Accept 值表示客戶端可以處理任何格式的媒體類型,首部字段User-Agent 值表示當前使用的HTTP客戶端信息爲"curl/7.70.0",GET請求報文主體爲空。
上面的響應報文狀態碼爲200 OK,表示請求已經正確處理完畢。首部字段Accept-Ranges 值表示服務器支持範圍請求;Connection 值表示該HTTP請求使用持久連接。
首部字段Cache-Control 值表示僅向特定用戶返回響應(private)、緩存會向源服務器進行有效期確認後處理資源(no-cache)、資源不進行緩存(no-store)、要求代理服務器對緩存的響應有效性再進行確認(proxy-revalidate)、代理服務器不可更改媒體類型(no-transform);Pragma 值表示客戶端要求所有的中間服務器不返回緩存的資源。
首部字段Content-Length 值表示響應報文主體長度爲2381 bytes;Content-Type 值表示響應報文主體的媒體類型爲 text/html;Date 值表示創建響應報文的日期時間爲"Fri, 08 May 2020 07:43:39 GMT";Etag 值表示請求資源實體的唯一標識爲"588604c4-94d";Last-Modified 值表示請求資源的最後修改時間爲"Mon, 23 Jan 2017 13:27:32 GMT";Server 值表示當前使用的HTTP服務器應用程序信息爲”bfe/1.0.8.18“。
首部字段Set-Cookie 是用來管理客戶端狀態信息的,上例中的值表示Cookie名稱及其值爲”BDORZ=27315“(也是需要進行狀態管理的請求報文中Cookie字段的值)、該Cookie的有效期爲86400 秒(max-age=86400)、該Cookie的適用域名爲".baidu.com"、該Cookie的適用目錄爲”/“。
2.6 HTTP/1.1 性能優化
看到這裏,你應該也發現了,HTTP報文的首部字段還是比較多的。前面也提到,現在每個Web頁面的平均資源數接近兩百,每請求一個資源都要發送一個HTTP請求報文,然後收到一個HTTP響應報文,這些報文中包含大量臃腫的首部字段(每個報文的平均首部長度大概500字節,如果算上Cookie字段,首部字段長度可能達到幾千字節),同一個Web頁面的不同資源請求/響應報文的首部字段有很多是重複的,來回傳輸這些重複臃腫的首部字段自然嚴重影響網絡利用率。
借鑑前面介紹的TCP持久連接技術提高網絡利用率的思路,可以讓不同資源請求共用一個TCP連接,也可以讓不同報文共用相同的首部字段,只傳遞不同的首部字段(甚至客戶端與服務器端都維護一份首部字段索引列表,只通過HTTP報文傳遞不同的首部字段索引號),可以將這種減少首部字段數據量的技術稱爲首部壓縮,HTTP/2協議便引入了首部壓縮技術HPACK,進一步提高網絡利用率,限於篇幅在後面的博文:HTTP/2原理詳解中進行詳細介紹。
三、HTTPS 安全與認證
3.1 HTTPS 原理
HTTP/1.1 協議默認是以明文方式傳輸數據的,這就有可能存在如下風險:
- 通信使用明文(不加密),內容可能會被竊聽;
- 不驗證通信方的身份,因此有可能遭遇僞裝;
- 無法證明報文的完整性,所以有可能已遭篡改。
我們要保證個人或組織的信息安全,也應該從這三方面着手想辦法,比如將明文加密傳輸,即便密文被截獲,對方沒有對應的密鑰也沒法解讀出有效信息(可參考密碼學博客:對稱加密與非對稱加密);要避免信息被篡改,可以將信息通過哈希算法生成一個消息認證碼(對消息的任何改動都很敏感),明文信息與對應的消息認證碼一同加密後傳輸,對方解密後計算有效信息的消息認證碼,並將計算結果與收到的消息認證碼比對,若二者不一致則說明信息已被篡改(可以參考博客:哈希算法能用來幹啥?);驗證通信方的身份可以通過數字簽名和第三方可信證書來確認。
HTTP 協議中沒有加密機制,但可以通過和 SSL(Secure Socket Layer)或 TLS(Transport Layer Security)的組合使用,加密 HTTP 的通信內容,用 SSL / TLS 建立安全通信線路之後,就可以在這條線路上進行 HTTP通信了。SSL / TLS 不僅提供加密處理,而且還使用證書(由值得信任的第三方機構頒發,用以證明服務器和客戶端是實際存在的)來驗證通信方的身份,使用哈希摘要算法(比如SHA-256、SHA-3等)來證明報文的完整性。與 SSL / TLS 組合使用的 HTTP 被稱爲 HTTPS(HTTPSecure),HTTPS 相當於身披 SSL / TLS 外殼的 HTTP,限於篇幅在下一篇博客中詳細介紹 TLS 協議加密原理 和 TLS 協議握手過程。
從TLS協議加密原理和握手過程可以瞭解到,HTTPS協議主要通過認證加密來保證通信的機密性和完整性,通過密鑰協商方案獲得認證加密所需的共享密鑰,通過證書認證與數字簽名算法來確認通信對端身份的真實合法性,看起來HTTPS對信息安全性的保護還是挺到位的。
隨着現在搭建網站的成本越來越低,網民數量越來越多,網絡攻擊、竊取用戶信息、網絡詐騙等手段更加氾濫,同時計算機性能和加密算法效率的提升,又降低了加密認證對網絡訪問效率的影響,HTTPS 的應用快速普及。HTTP/2 更是被各大瀏覽器廠商默認強制使用TLS 網絡安全協議,你如果留心會發現,我們日常訪問的大多數網站都使用了HTTPS協議(可以通過網址URL 開頭的https://判斷,也可以通過瀏覽器地址輸入框的安全鎖標誌判斷)。
TLS 協議雖然可以保證信息的安全傳輸,也能驗證客戶端與服務器身份的真實合法性,也即可以保證網絡訪問設備的真實合法性,但無法確保使用這臺設備的人就是被授權的用戶。某些敏感信息比如個人銀行賬戶登錄轉賬等,銀行服務端需要確認登錄該賬號的必須是賬號擁有者本人,單純確認用戶經常使用的設備是不夠的,如果手機丟失或者別人借用我們手機,也要能保證對方不能訪問我們的銀行賬戶等敏感信息,這就需要Web認證功能。
3.2 用戶身份認證
計算機本身無法判斷坐在顯示器前的使用者的身份,也無法確認網絡的那頭究竟有誰。可見,爲了弄清究竟是誰在訪問服務器,就得讓對方的客戶端自報家門。爲確認訪問用戶本人是否真的具有訪問系統的權限,就需要覈對“登錄者本人才知道的信息”、“登錄者本人才會有的信息”等,覈對的信息通常是指以下這些:
- 密碼:只有本人才會知道的字符串信息;
- 動態令牌:僅限本人持有的密碼設備內顯示的一次性密碼;
- 數字證書:僅限本人使用的網絡訪問設備的身份認證信息;
- 生物認證:指紋、面部、虹膜等本人的生理信息;
- IC 卡:僅限本人持有的密碼設備信息,比如門禁卡。
最常用的核對用戶身份信息的方式是”用戶名 — 密碼“信息,這個密碼可以是普通密碼、動態令牌密碼、生理信息密碼等,這些信息是如何在客戶端與服務器之間傳輸的呢?前面介紹過請求報文首部中的Authorization、Proxy-Authorization 字段和響應報文首部中的Proxy-Authenticate、WWW-Authenticate 字段嗎?這幾個字段就是用來傳輸用戶身份認證信息的。
用戶名和密碼等用戶身份認證信息是比較敏感,需要嚴格保密的,假如這些信息被別人獲知,就可以憑藉這些信息通過用戶的身份認證,計算機會默認是出自本人的行爲,因此掌控機密信息的密碼絕不能讓他人得到,更不能輕易地就被破解出來。既然需要用戶身份認證信息需要嚴格保密,就需要對這些敏感信息進行加密傳輸,而不能使用明文傳輸,根據加密處理方式不同,可以將用戶認證分爲如下幾種類型:
- BASIC 認證(基本認證):用戶認證信息採用 Base64 編碼方式,由於不需要任何附加信息即可對其解碼獲得用戶認證信息,安全性較差,極少使用(前面介紹報文首部認證字段便是以BASIC認證作爲示例);
- DIGEST 認證(摘要認證):用戶認證信息和服務器通過401狀態碼返回的一次性質詢碼一起進行單向哈希運算,將計算出的哈希摘要信息作爲響應碼發送給服務器驗證,即便響應碼被截獲也很難解碼出用戶認證信息,密碼泄露可能性大大降低。但DIGEST 認證並不存在防止用戶僞裝的保護機制,攻擊者可以截獲響應碼並僞裝成用戶通過服務器的認證,安全性依然較弱,使用範圍有限(下圖是DIGEST認證步驟);
- TLS 客戶端證書認證:服務器通過發送 Certificate Request 報文,要求客戶端提供數字證書,客戶端會以 Client Certificate 和Client CertificateVerify報文將自己的數字證書和數字簽名信息發送給服務器。數字證書內包含第三方可信機構對客戶端的審覈簽名信息和客戶端的公鑰信息,數字簽名是使用客戶端私鑰加密後的信息,可以通過數字證書內包含的公鑰解密,因爲只有客戶端持有私鑰,所以數字簽名能確認客戶端的身份。但單純靠數字證書只能確認客戶端設備的身份,不能確認使用該客戶端設備的用戶身份,因此TLS 客戶端證書認證常與下面要介紹的基於表單的認證組合使用,這就是雙因素認證(在TLS 握手過程中介紹了TLS 客戶端證書認證的步驟);
- FormBase 認證(基於表單認證):該認證方法並不是在 HTTP 協議中定義的,而是由Web 應用程序各自實現的,客戶端會通過POST方法的請求報文主體向服務器上的 Web 應用程序發送登錄/認證信息(Credential),服務器端會按登錄信息的驗證結果認證。服務器端認證通過後,可以生成一個與用戶認證狀態綁定的Session ID並記錄在服務器端,Session ID可以通過Set-Cookie 字段返回給客戶端,並作爲Cookie 保存在客戶端本地,客戶端在Session ID有效期內可在請求報文中附加Cookie信息作爲服務器識別用戶及其認證狀態的標誌。用戶認證信息和Cookie 信息也有被截獲並僞裝的風險,因此基於表單的認證常與HTTPS 組合使用,保證用戶認證信息和Cookie 信息的安全,Set-Cookie字段屬性中的Secure和HttpOnly就是爲了保證Cookie 信息安全的(可以參考前文介紹的Cookie狀態管理)。
HTTPS協議可以保證信息的安全傳輸,同時能通過證書和簽名驗證客戶端與服務器身份的真實合法性,協議本身不能驗證訪問用戶身份的問題可以通過基於表單的認證方式覈對登錄者本人才知道或纔會有的信息解決,藉助Cookie 進行登錄/認證狀態管理,還可以讓我們訪問同一域名時免去次次都需要登錄認證的麻煩,爲用戶授權訪問提供了極大的便利。
3.3 HSTS 原理
HTTPS 協議雖然提高了通信的安全性,但降低了網絡訪問效率,申請數字證書也是有成本的,所以早期除了對通信安全比較敏感的網站(比如跟網購、支付相關的)使用HTTPS協議外,更多的網站使用普通的HTTP 協議進行通信,畢竟HTTP 協議有更低的成本和更高的網絡訪問效率。
隨着計算機性能的提升和大家對網絡安全的重視,大多數網站都更新爲HTTPS 協議,但有些超鏈接更新比較滯後,用戶輸入網址可能也習慣輸入”http://“,爲了保證向前兼容不至於影響用戶的正常訪問,網站就需要對HTTP 請求進行重定向,藉助301狀態碼的響應報文將"http://"請求URL 重定向到 ”https://“請求URL。這種將HTTP請求重定向到HTTPS請求的方案比較簡單,能解決前向兼容問題,但首次發送的是HTTP請求報文,明文傳輸的HTTP重定向報文很容易被劫持並篡改重定向URL,存在被攻擊的風險:
要解決HTTP請求劫持問題,很容易想到只要在請求過程中不出現HTTP請求,直接以HTTPS請求進行通信,就可以避免上面的攻擊。HSTS(HTTP Strict Transport Security)便是按照上述邏輯實現的Web安全策略,HSTS規範於2012年由IETF公佈爲RFC6797。網站採用 HSTS 後,用戶訪問時無需手動在地址欄中輸入 HTTPS,瀏覽器會自動採用 HTTPS 訪問網站地址,從而保證用戶始終訪問到網站的加密鏈接,保護數據傳輸安全。
HSTS最爲核心的是一個HTTP響應頭(HTTP Response Header),正是它可以讓瀏覽器得知,在接下來的一段時間內(由max-age參數定義),當前域名只能通過HTTPS進行訪問,並且在瀏覽器發現當前連接不安全的情況下,強制拒絕用戶的後續訪問要求。HSTS Header的語法如下:
/* 此響應頭只有在 https 訪問返回時才生效,其中[ ]中的參數表示可選;
* max-age是必選參數,是一個以秒爲單位的數值,它代表着HSTS Header的過期時間,通常設置爲1年,即31536000秒;
* includeSubDomains是可選參數,如果包含它,則意味着當前域名及其子域名均開啓HSTS保護;
* preload是可選參數,只有當你申請將自己的域名加入到瀏覽器內置列表的時候才需要使用到它。
*/
Strict-Transport-Security: <max-age=>[; includeSubDomains][; preload]
HSTS完整流程如下(以max-age有效期1年、當前域名及其子域名均開啓HSTS保護爲例,下圖取自博文:HSTS詳解):
只要是在有效期內,瀏覽器都將直接強制性的發起HTTPS請求,假如有效期過了怎麼辦呢?因爲HSTS Header存在於每個響應中,隨着用戶和網站的交互,這個有效時間時刻都在刷新,再加上有效期通常都被設置成了1年,所以只要用戶的前後兩次請求之間的時間間隔沒有超過1年,則基本上不會出現安全風險。就算超過了有效期,但是隻要用戶和網站再進行一次新的交互,用戶的瀏覽器又將開啓有效期爲1年的HSTS保護。
細心的你可能發現了,HSTS存在一個比較薄弱的環節,那就是瀏覽器沒有當前網站的HSTS信息的時候,或者第一次訪問網站的時候,依然需要一次明文的HTTP請求和重定向才能切換到HTTPS,以及刷新HSTS信息。而就是這麼一瞬間卻給攻擊者留下了可乘之機,使得他們可以把這一次的HTTP請求劫持下來,繼續中間人攻擊。
針對這種攻擊,HSTS也有應對辦法,那就是在瀏覽器裏內置一個預加載列表HSTS Preload List,只要是在這個預加載列表裏的域名,無論何時、何種情況,瀏覽器都只使用HTTPS發起連接。這個預加載列表由Google Chromium維護(FireFox、Safari、Edge等主流瀏覽器均在使用),你的網站如果滿足相應條件(具體條件可參考網址:https://hstspreload.org/)可以通過HSTS Header 的preload 參數申請將網站域名加入到HSTS Preload List中,審覈通過後你的網站就成功加入到預加載列表中了,以後訪問你的網站及其所有子網站都強制使用HTTPS發起請求,讓HTTP請求劫持攻擊無處下手。
下面使用Microsoft Chromium Edge瀏覽器自帶的Network工具(設置及其他 --> 更多工具 --> 開發人員工具 --> 網絡)查看https報文的標頭字段信息,可以在響應標頭中看到strict-transport-security字段的信息如下:
更多文章:
- 《Web技術(五):HTTP/2 是如何解決HTTP/1.1 性能瓶頸的?》
- 《Web技術(三):TLS 加密原理(AES-GCM + ECDHE-ECDSA/RSA)》
- 《Web技術(一):簡史與技術要素(URL + HTML + HTTP)》
- 《IOT-OS之RT-Thread(十七)— 如何使用HTTP協議實現OTA空中升級》
- 《HTTPS 溫故知新(一) —— 開篇》
- 《HTTPS 溫故知新(二) —— TLS 記錄層協議》
- 《HTTPS 溫故知新(三) —— 直觀感受 TLS 握手流程(上)》
- 《HTTPS 溫故知新(四) —— 直觀感受 TLS 握手流程(下)》
- 《HTTPS 溫故知新(五) —— TLS 中的密鑰計算》
- 《HTTPS 溫故知新(六) —— TLS 中的 Extensions》
- 《全雙工通信的 WebSocket》