HTTP 知(zhī)識體(tǐ)系
HTTP 知(zhī)識體(tǐ)系
這是本文的思維導圖:
001. HTTP 報文結構是怎樣的?
對于 TCP 而言,在傳輸的時候分(fēn)爲兩個部分(fēn):TCP頭和數據部分(fēn)。
而 HTTP 類似,也是header + body的結構,具體(tǐ)而言:
起始行 + 頭部 + 空行 + 實體(tǐ)
由于 http 請求報文和響應報文是有一(yī)定區别,因此我(wǒ)(wǒ)們分(fēn)開(kāi)介紹。
起始行
對于請求報文來說,起始行類似下(xià)面這樣:
GET /home HTTP/1.1
也就是方法 + 路徑 + http版本。
對于響應報文來說,起始行一(yī)般張這個樣:
HTTP/1.1 200 OK
響應報文的起始行也叫做狀态行。由http版本、狀态碼和原因三部分(fēn)組成。
值得注意的是,在起始行中(zhōng),每兩個部分(fēn)之間用空格隔開(kāi),最後一(yī)個部分(fēn)後面應該接一(yī)個換行,嚴格遵循ABNF語法規範
頭部
展示一(yī)下(xià)請求頭和響應頭在報文中(zhōng)的位置:
不管是請求頭還是響應頭,其中(zhōng)的字段是相當多的,而且牽扯到http非常多的特性,這裏就不一(yī)一(yī)列舉的,重點看看這些頭部字段的格式:
1. 字段名不區分(fēn)大(dà)小(xiǎo)寫
1. 字段名不允許出現空格,不可以出現下(xià)劃線_
1. 字段名後面必須緊接着:
空行
很重要,用來區分(fēn)開(kāi)頭部和實體(tǐ)。
問: 如果說在頭部中(zhōng)間故意加一(yī)個空行會怎麽樣?
那麽空行後的内容全部被視爲實體(tǐ)。
實體(tǐ)
就是具體(tǐ)的數據了,也就是body部分(fēn)。請求報文對應請求體(tǐ), 響應報文對應響應體(tǐ)
002. 如何理解 HTTP 的請求方法?
有哪些請求方法?
http/1.1規定了以下(xià)請求方法(注意,都是大(dà)寫):
• GET: 通常用來獲取資(zī)源
• HEAD: 獲取資(zī)源的元信息
• POST: 提交數據,即上傳數據
• PUT: 修改數據
• DELETE: 删除資(zī)源(幾乎用不到)
• CONNECT: 建立連接隧道,用于代理服務器
• OPTIONS: 列出可對資(zī)源實行的請求方法,用來跨域請求
• TRACE: 追蹤請求-響應的傳輸路徑
GET 和 POST 有什麽區别?
首先最直觀的是語義上的區别。
而後又(yòu)有這樣一(yī)些具體(tǐ)的差别:
• 從緩存的角度,GET 請求會被浏覽器主動緩存下(xià)來,留下(xià)曆史記錄,而 POST 默認不會。
• 從編碼的角度,GET 隻能進行 URL 編碼,隻能接收 ASCII 字符,而 POST 沒有限制。
• 從參數的角度,GET 一(yī)般放(fàng)在 URL 中(zhōng),因此不安全,POST 放(fàng)在請求體(tǐ)中(zhōng),更适合傳輸敏感信息。
• 從幂等性的角度,GET是幂等的,而POST不是。(幂等表示執行相同的操作,結果也是相同的)
• 從TCP的角度,GET 請求會把請求報文一(yī)次性發出去(qù),而 POST 會分(fēn)爲兩個 TCP 數據包,首先發 header 部分(fēn),如果服務器響應 100(continue), 然後發 body 部分(fēn)。(火(huǒ)狐浏覽器除外(wài),它的 POST 請求隻發一(yī)個 TCP 包)
003: 如何理解 URI?
URI, 全稱爲(Uniform Resource Identifier), 也就是統一(yī)資(zī)源标識符,它的作用很簡單,就是區分(fēn)互聯網上不同的資(zī)源。
但是,它并不是我(wǒ)(wǒ)們常說的網址, 網址指的是URL, 實際上URI包含了URN和URL兩個部分(fēn),由于 URL 過于普及,就默認将 URI 視爲 URL 了。
URI 的結構
URI 真正最完整的結構是這樣的。
scheme 表示協議名,比如http, https, file等等。後面必須和://連在一(yī)起。
user:passwd@ 表示登錄主機時的用戶信息,不過很不安全,不推薦使用,也不常用。
host:port表示主機名和端口。
path表示請求路徑,标記資(zī)源所在位置。
query表示查詢參數,爲key=val這種形式,多個鍵值對之間用&隔開(kāi)。
fragment表示 URI 所定位的資(zī)源内的一(yī)個錨點,浏覽器可以根據這個錨點跳轉到對應的位置。
舉個例子:
https://www.baidu.com/s?wd=HTTP&rsv_spt=1
這個 URI 中(zhōng),https即scheme部分(fēn),www.baidu.com爲host:port部分(fēn)(注意,http 和 https 的默認端口分(fēn)别爲80、443),/s爲path部分(fēn),而wd=HTTP&rsv_spt=1就是query部分(fēn)。
URI 編碼
URI 隻能使用ASCII, ASCII 之外(wài)的字符是不支持顯示的,而且還有一(yī)部分(fēn)符号是界定符,如果不加以處理就會導緻解析出錯。
因此,URI 引入了編碼機制,将所有非 ASCII 碼字符和界定符轉爲十六進制字節值,然後在前面加個%。
如,空格被轉義成了%20,三元被轉義成了%E4%B8%89%E5%85%83。
004: 如何理解 HTTP 狀态碼?
RFC 規定 HTTP 的狀态碼爲三位數,被分(fēn)爲五類:
• 1xx: 表示目前是協議處理的中(zhōng)間狀态,還需要後續操作。
• 2xx: 表示成功狀态。
• 3xx: 重定向狀态,資(zī)源位置發生(shēng)變動,需要重新請求。
• 4xx: 請求報文有誤。
• 5xx: 服務器端發生(shēng)錯誤。
1xx
101 Switching Protocols。在HTTP升級爲WebSocket的時候,如果服務器同意變更,就會發送狀态碼 101。
2xx
200 OK是見得最多的成功狀态碼。通常在響應體(tǐ)中(zhōng)放(fàng)有數據。
204 No Content含義與 200 相同,但響應頭後沒有 body 數據。
206 Partial Content顧名思義,表示部分(fēn)内容,它的使用場景爲 HTTP 分(fēn)塊下(xià)載和斷點續傳,當然也會帶上相應的響應頭字段Content-Range。
3xx
301 Moved Permanently即永久重定向,對應着302 Found,即臨時重定向。
比如你的網站從 HTTP 升級到了 HTTPS 了,以前的站點再也不用了,應當返回301,這個時候浏覽器默認會做緩存優化,在第二次訪問的時候自動訪問重定向的那個地址。
而如果隻是暫時不可用,那麽直接返回302即可,和301不同的是,浏覽器并不會做緩存優化。
304 Not Modified: 當協商(shāng)緩存命中(zhōng)時會返回這個狀态碼。詳見浏覽器緩存
4xx
400 Bad Request: 開(kāi)發者經常看到一(yī)頭霧水,隻是籠統地提示了一(yī)下(xià)錯誤,并不知(zhī)道哪裏出錯了。
403 Forbidden: 這實際上并不是請求報文出錯,而是服務器禁止訪問,原因有很多,比如法律禁止、信息敏感。
404 Not Found: 資(zī)源未找到,表示沒在服務器上找到相應的資(zī)源。
405 Method Not Allowed: 請求方法不被服務器端允許。
406 Not Acceptable: 資(zī)源無法滿足客戶端的條件。
408 Request Timeout: 服務器等待了太長時間。
409 Conflict: 多個請求發生(shēng)了沖突。
413 Request Entity Too Large: 請求體(tǐ)的數據過大(dà)。
414 Request-URI Too Long: 請求行裏的 URI 太大(dà)。
429 Too Many Request: 客戶端發送的請求過多。
431 Request Header Fields Too Large請求頭的字段内容太大(dà)。
5xx
500 Internal Server Error: 僅僅告訴你服務器出錯了,出了啥錯咱也不知(zhī)道。
501 Not Implemented: 表示客戶端請求的功能還不支持。
502 Bad Gateway: 服務器自身是正常的,但訪問的時候出錯了,啥錯誤咱也不知(zhī)道。
503 Service Unavailable: 表示服務器當前很忙,暫時無法響應服務。
005: 簡要概括一(yī)下(xià) HTTP 的特點?HTTP 有哪些缺點?
HTTP 特點
HTTP 的特點概括如下(xià):
1. 靈活可擴展,主要體(tǐ)現在兩個方面。一(yī)個是語義上的自由,隻規定了基本格式,比如空格分(fēn)隔單詞,換行分(fēn)隔字段,其他的各個部分(fēn)都沒有嚴格的語法限制。另一(yī)個是傳輸形式的多樣性,不僅僅可以傳輸文本,還能傳輸圖片、視頻(pín)等任意數據,非常方便。
2. 可靠傳輸。HTTP 基于 TCP/IP,因此把這一(yī)特性繼承了下(xià)來。這屬于 TCP 的特性,不具體(tǐ)介紹了。
3. 請求-應答。也就是一(yī)發一(yī)收、有來有回, 當然這個請求方和應答方不單單指客戶端和服務器之間,如果某台服務器作爲代理來連接後端的服務端,那麽這台服務器也會扮演請求方的角色。
4. 無狀态。這裏的狀态是指通信過程的上下(xià)文信息,而每次 http 請求都是獨立、無關的,默認不需要保留狀态信息。
HTTP 缺點
無狀态
所謂的優點和缺點還是要分(fēn)場景來看的,對于 HTTP 而言,最具争議的地方在于它的無狀态。
在需要長連接的場景中(zhōng),需要保存大(dà)量的上下(xià)文信息,以免傳輸大(dà)量重複的信息,那麽這時候無狀态就是 http 的缺點了。
但與此同時,另外(wài)一(yī)些應用僅僅隻是爲了獲取一(yī)些數據,不需要保存連接上下(xià)文信息,無狀态反而減少了網絡開(kāi)銷,成爲了 http 的優點。
明文傳輸
即協議裏的報文(主要指的是頭部)不使用二進制數據,而是文本形式。
這當然對于調試提供了便利,但同時也讓 HTTP 的報文信息暴露給了外(wài)界,給攻擊者也提供了便利。WIFI陷阱就是利用 HTTP 明文傳輸的缺點,誘導你連上熱點,然後瘋狂抓你所有的流量,從而拿到你的敏感信息。
隊頭阻塞問題
當 http 開(kāi)啓長連接時,共用一(yī)個 TCP 連接,同一(yī)時刻隻能處理一(yī)個請求,那麽當前請求耗時過長的情況下(xià),其它的請求隻能處于阻塞狀态,也就是著名的隊頭阻塞問題。接下(xià)來會有一(yī)小(xiǎo)節讨論這個問題。
006: 對 Accept 系列字段了解多少?
對于Accept系列字段的介紹分(fēn)爲四個部分(fēn): 數據格式、壓縮方式、支持語言和字符集。
數據格式
上一(yī)節談到 HTTP 靈活的特性,它支持非常多的數據格式,那麽這麽多格式的數據一(yī)起到達客戶端,客戶端怎麽知(zhī)道它的格式呢?
當然,最低效的方式是直接猜,有沒有更好的方式呢?直接指定可以嗎(ma)?
答案是肯定的。不過首先需要介紹一(yī)個标準——MIME(Multipurpose Internet Mail Extensions, 多用途互聯網郵件擴展)。它首先用在電(diàn)子郵件系統中(zhōng),讓郵件可以發任意類型的數據,這對于 HTTP 來說也是通用的。
因此,HTTP 從MIME type取了一(yī)部分(fēn)來标記報文 body 部分(fēn)的數據類型,這些類型體(tǐ)現在Content-Type這個字段,當然這是針對于發送端而言,接收端想要收到特定類型的數據,也可以用Accept字段。
具體(tǐ)而言,這兩個字段的取值可以分(fēn)爲下(xià)面幾類:
• text: text/html, text/plain, text/css 等
• image: image/gif, image/jpeg, image/png 等
• audio/video: audio/mpeg, video/mp4 等
• application: application/json, application/javascript, application/pdf, application/octet-stream
壓縮方式
當然一(yī)般這些數據都是會進行編碼壓縮的,采取什麽樣的壓縮方式就體(tǐ)現在了發送方的Content-Encoding字段上, 同樣的,接收什麽樣的壓縮方式體(tǐ)現在了接受方的Accept-Encoding字段上。這個字段的取值有下(xià)面幾種:
• gzip: 當今最流行的壓縮格式
• deflate: 另外(wài)一(yī)種著名的壓縮格式
• br: 一(yī)種專門爲 HTTP 發明的壓縮算法
// 發送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip
支持語言
對于發送方而言,還有一(yī)個Content-Language字段,在需要實現國際化的方案當中(zhōng),可以用來指定支持的語言,在接受方對應的字段爲Accept-Language。如:
// 發送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en
字符集
最後是一(yī)個比較特殊的字段, 在接收端對應爲Accept-Charset,指定可以接受的字符集,而在發送端并沒有對應的Content-Charset, 而是直接放(fàng)在了Content-Type中(zhōng),以charset屬性指定。如:
// 發送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8
以一(yī)張圖來總結一(yī)下(xià)吧:
007: 對于定長和不定長的數據,HTTP 是怎麽傳輸的?
定長包體(tǐ)
對于定長包體(tǐ)而言,發送端在傳輸的時候一(yī)般會帶上 Content-Length, 來指明包體(tǐ)的長度。
我(wǒ)(wǒ)們用一(yī)個nodejs服務器來模拟一(yī)下(xià):
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("helloworld");
}
})
server.listen(8081, () => {
console.log("成功啓動");
})
啓動後訪問: localhost:8081。
浏覽器中(zhōng)顯示如下(xià):
helloworld
這是長度正确的情況,那不正确的情況是如何處理的呢?
我(wǒ)(wǒ)們試着把這個長度設置的小(xiǎo)一(yī)些:
res.setHeader('Content-Length', 8);
重啓服務,再次訪問,現在浏覽器中(zhōng)内容如下(xià):
helloworld
那後面的ld哪裏去(qù)了呢?實際上在 http 的響應體(tǐ)中(zhōng)直接被截去(qù)了。
然後我(wǒ)(wǒ)們試着将這個長度設置得大(dà)一(yī)些:
res.setHeader('Content-Length', 12);
此時浏覽器直接無法顯示了。可以看到Content-Length對于 http 傳輸過程起到了十分(fēn)關鍵的作用,如果設置不當可以直接導緻傳輸失敗。
不定長包體(tǐ)
上述是針對于定長包體(tǐ),那麽對于不定長包體(tǐ)而言是如何傳輸的呢?
這裏就必須介紹另外(wài)一(yī)個 http 頭部字段了:
Transfer-Encoding: chunked
表示分(fēn)塊傳輸數據,設置這個字段後會自動産生(shēng)兩個效果:
• Content-Length 字段會被忽略
• 基于長連接持續推送動态内容
我(wǒ)(wǒ)們依然以一(yī)個實際的例子來模拟分(fēn)塊傳輸,nodejs 程序如下(xià):
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("<p>來啦</p>");
setTimeout(() => {
res.write("第一(yī)次傳輸<br/>");
}, 1000);
setTimeout(() => {
res.write("第二次傳輸");
res.end()
}, 2000);
}
})
server.listen(8009, () => {
console.log("成功啓動");
})
用 telnet 抓到的響應如下(xià):
注意,Connection: keep-alive及之前的爲響應行和響應頭,後面的内容爲響應體(tǐ),這兩部分(fēn)用換行符隔開(kāi)。
響應體(tǐ)的結構比較有意思,如下(xià)所示:
chunk長度(16進制的數)
第一(yī)個chunk的内容
chunk長度(16進制的數)
第二個chunk的内容
......
0
最後是留有有一(yī)個空行的,這一(yī)點請注意。
以上便是 http 對于定長數據和不定長數據的傳輸方式。
008: HTTP 如何處理大(dà)文件的傳輸?
對于幾百 M 甚至上 G 的大(dà)文件來說,如果要一(yī)口氣全部傳輸過來顯然是不現實的,會有大(dà)量的等待時間,嚴重影響用戶體(tǐ)驗。因此,HTTP 針對這一(yī)場景,采取了範圍請求的解決方案,允許客戶端僅僅請求一(yī)個資(zī)源的一(yī)部分(fēn)。
如何支持
當然,前提是服務器要支持範圍請求,要支持這個功能,就必須加上這樣一(yī)個響應頭:
Accept-Ranges: none
用來告知(zhī)客戶端這邊是支持範圍請求的。
Range 字段拆解
而對于客戶端而言,它需要指定請求哪一(yī)部分(fēn),通過Range這個請求頭字段确定,格式爲bytes=x-y。接下(xià)來就來讨論一(yī)下(xià)這個 Range 的書(shū)寫格式:
• 0-499表示從開(kāi)始到第 499 個字節。
• 500- 表示從第 500 字節到文件終點。
• -100表示文件的最後100個字節。
服務器收到請求之後,首先驗證範圍是否合法,如果越界了那麽返回416錯誤碼,否則讀取相應片段,返回206狀态碼。
同時,服務器需要添加Content-Range字段,這個字段的格式根據請求頭中(zhōng)Range字段的不同而有所差異。
具體(tǐ)來說,請求單段數據和請求多段數據,響應頭是不一(yī)樣的。
舉個例子:
// 單段數據
Range: bytes=0-9
// 多段數據
Range: bytes=0-9, 30-39
接下(xià)來我(wǒ)(wǒ)們就分(fēn)别來讨論着兩種情況。
單段數據
對于單段數據的請求,返回的響應如下(xià):
接下(xià)來我(wǒ)(wǒ)們就分(fēn)别來讨論着兩種情況。
單段數據
對于單段數據的請求,返回的響應如下(xià):
值得注意的是Content-Range字段,0-9表示請求的返回,100表示資(zī)源的總大(dà)小(xiǎo),很好理解。
多段數據
接下(xià)來我(wǒ)(wǒ)們看看多段請求的情況。得到的響應會是下(xià)面這個形式:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
eex jspy e
--00000010101--
這個時候出現了一(yī)個非常關鍵的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是這樣的:
• 請求一(yī)定是多段數據請求
• 響應體(tǐ)中(zhōng)的分(fēn)隔符是 00000010101
因此,在響應體(tǐ)中(zhōng)各段數據之間會由這裏指定的分(fēn)隔符分(fēn)開(kāi),而且在最後的分(fēn)隔末尾添上--表示結束。
以上就是 http 針對大(dà)文件傳輸所采用的手段.
009: HTTP 中(zhōng)如何處理表單數據的提交?
在 http 中(zhōng),有兩種主要的表單提交的方式,體(tǐ)現在兩種不同的Content-Type取值:
• application/x-www-form-urlencoded
• multipart/form-data
由于表單提交一(yī)般是POST請求,很少考慮GET,因此這裏我(wǒ)(wǒ)們将默認提交的數據放(fàng)在請求體(tǐ)中(zhōng)。
application/x-www-form-urlencoded
對于application/x-www-form-urlencoded格式的表單内容,有以下(xià)特點:
• 其中(zhōng)的數據會被編碼成以&分(fēn)隔的鍵值對
• 字符以URL編碼方式編碼。
如:
// 轉換過程: {a: 1, b: 2} -> a=1&b=2 -> 如下(xià)(最終形式)
"a%3D1%26b%3D2"
multipart/form-data
對于multipart/form-data而言:
• 請求頭中(zhōng)的Content-Type字段會包含boundary,且boundary的值有浏覽器默認指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe。
• 數據會分(fēn)爲多個部分(fēn),每兩個部分(fēn)之間通過分(fēn)隔符來分(fēn)隔,每部分(fēn)表述均有 HTTP 頭部描述子包體(tǐ),如Content-Type,在最後的分(fēn)隔符會加上--表示結束。
相應的請求體(tǐ)是下(xià)面這樣:
Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
小(xiǎo)結
值得一(yī)提的是,multipart/form-data 格式最大(dà)的特點在于:每一(yī)個表單元素都是獨立的資(zī)源表述。另外(wài),你可能在寫業務的過程中(zhōng),并沒有注意到其中(zhōng)還有boundary的存在,如果你打開(kāi)抓包工(gōng)具,确實可以看到不同的表單元素被拆分(fēn)開(kāi)了,之所以在平時感覺不到,是以爲浏覽器和 HTTP 給你封裝了這一(yī)系列操作。
而且,在實際的場景中(zhōng),對于圖片等文件的上傳,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因爲沒有必要做 URL 編碼,帶來巨大(dà)耗時的同時也占用了更多的空間。
010: HTTP1.1 如何解決 HTTP 的隊頭阻塞問題?
什麽是 HTTP 隊頭阻塞?
從前面的小(xiǎo)節可以知(zhī)道,HTTP 傳輸是基于請求-應答的模式進行的,報文必須是一(yī)發一(yī)收,但值得注意的是,裏面的任務被放(fàng)在一(yī)個任務隊列中(zhōng)串行執行,一(yī)旦隊首的請求處理太慢(màn),就會阻塞後面請求的處理。這就是著名的HTTP隊頭阻塞問題。
并發連接
對于一(yī)個域名允許分(fēn)配多個長連接,那麽相當于增加了任務隊列,不至于一(yī)個隊伍的任務阻塞其它所有任務。在RFC2616規定過客戶端最多并發 2 個連接,不過事實上在現在的浏覽器标準中(zhōng),這個上限要多很多,Chrome 中(zhōng)是 6 個。
但其實,即使是提高了并發連接,還是不能滿足人們對性能的需求。
域名分(fēn)片
一(yī)個域名不是可以并發 6 個長連接嗎(ma)?那我(wǒ)(wǒ)就多分(fēn)幾個域名。
比如 content1.sanyuan.com 、content2.sanyuan.com。
這樣一(yī)個sanyuan.com域名下(xià)可以分(fēn)出非常多的二級域名,而它們都指向同樣的一(yī)台服務器,能夠并發的長連接數更多了,事實上也更好地解決了隊頭阻塞的問題。
011: 對 Cookie 了解多少?
Cookie 簡介
前面說到了 HTTP 是一(yī)個無狀态的協議,每次 http 請求都是獨立、無關的,默認不需要保留狀态信息。但有時候需要保存一(yī)些狀态,怎麽辦呢?
HTTP 爲此引入了 Cookie。Cookie 本質上就是浏覽器裏面存儲的一(yī)個很小(xiǎo)的文本文件,内部以鍵值對的方式來存儲(在chrome開(kāi)發者面闆的Application這一(yī)欄可以看到)。向同一(yī)個域名下(xià)發送請求,都會攜帶相同的 Cookie,服務器拿到 Cookie 進行解析,便能拿到客戶端的狀态。而服務端可以通過響應頭中(zhōng)的Set-Cookie字段來對客戶端寫入Cookie。舉例如下(xià):
// 請求頭
Cookie: a=xxx;b=xxx
// 響應頭
Set-Cookie: a=xxx
set-Cookie: b=xxx
Cookie 屬性
生(shēng)存周期
Cookie 的有效期可以通過Expires和Max-Age兩個屬性來設置。
• Expires即過期時間
• Max-Age用的是一(yī)段時間間隔,單位是秒,從浏覽器收到報文開(kāi)始計算。
若 Cookie 過期,則這個 Cookie 會被删除,并不會發送給服務端。
作用域
關于作用域也有兩個屬性: Domain和path, 給 Cookie 綁定了域名和路徑,在發送請求之前,發現域名或者路徑和這兩個屬性不匹配,那麽就不會帶上 Cookie。值得注意的是,對于路徑來說,/表示域名下(xià)的任意路徑都允許使用 Cookie。
安全相關
如果帶上Secure,說明隻能通過 HTTPS 傳輸 cookie。
如果 cookie 字段帶上HttpOnly,那麽說明隻能通過 HTTP 協議傳輸,不能通過 JS 訪問,這也是預防 XSS 攻擊的重要手段。
相應的,對于 CSRF 攻擊的預防,也有SameSite屬性。
SameSite可以設置爲三個值,Strict、Lax和None。
a. 在Strict模式下(xià),浏覽器完全禁止第三方請求攜帶Cookie。比如請求sanyuan.com網站隻能在sanyuan.com域名當中(zhōng)請求才能攜帶 Cookie,在其他網站請求都不能。
b. 在Lax模式,就寬松一(yī)點了,但是隻能在 get 方法提交表單況或者a 标簽發送 get 請求的情況下(xià)可以攜帶 Cookie,其他情況均不能。
c. 在None模式下(xià),也就是默認模式,請求會自動攜帶上 Cookie。
Cookie 的缺點
1. 容量缺陷。Cookie 的體(tǐ)積上限隻有4KB,隻能用來存儲少量的信息。
2. 性能缺陷。Cookie 緊跟域名,不管域名下(xià)面的某一(yī)個地址需不需要這個 Cookie ,請求都會攜帶上完整的 Cookie,這樣随着請求數的增多,其實會造成巨大(dà)的性能浪費(fèi)的,因爲請求攜帶了很多不必要的内容。但可以通過Domain和Path指定作用域來解決。
3. 安全缺陷。由于 Cookie 以純文本的形式在浏覽器和服務器中(zhōng)傳遞,很容易被非法用戶截獲,然後進行一(yī)系列的篡改,在 Cookie 的有效期内重新發送給服務器,這是相當危險的。另外(wài),在HttpOnly爲 false 的情況下(xià),Cookie 信息能直接通過 JS 腳本來讀取。
012: 如何理解 HTTP 代理?
我(wǒ)(wǒ)們知(zhī)道在 HTTP 是基于請求-響應模型的協議,一(yī)般由客戶端發請求,服務器來進行響應。
當然,也有特殊情況,就是代理服務器的情況。引入代理之後,作爲代理的服務器相當于一(yī)個中(zhōng)間人的角色,對于客戶端而言,表現爲服務器進行響應;而對于源服務器,表現爲客戶端發起請求,具有雙重身份。
那代理服務器到底是用來做什麽的呢?
功能
1. 負載均衡。客戶端的請求隻會先到達代理服務器,後面到底有多少源服務器,IP 都是多少,客戶端是不知(zhī)道的。因此,這個代理服務器可以拿到這個請求之後,可以通過特定的算法分(fēn)發給不同的源服務器,讓各台源服務器的負載盡量平均。當然,這樣的算法有很多,包括随機算法、輪詢、一(yī)緻性hash、LRU(最近最少使用)等等,不過這些算法并不是本文的重點,大(dà)家有興趣自己可以研究一(yī)下(xià)。
2. 保障安全。利用心跳機制監控後台的服務器,一(yī)旦發現故障機就将其踢出集群。并且對于上下(xià)行的數據進行過濾,對非法 IP 限流,這些都是代理服務器的工(gōng)作。
3. 緩存代理。将内容緩存到代理服務器,使得客戶端可以直接從代理服務器獲得而不用到源服務器那裏。下(xià)一(yī)節詳細拆解。
相關頭部字段
Via
代理服務器需要标明自己的身份,在 HTTP 傳輸中(zhōng)留下(xià)自己的痕迹,怎麽辦呢?
通過Via字段來記錄。舉個例子,現在中(zhōng)間有兩台代理服務器,在客戶端發送請求後會經曆這樣一(yī)個過程:
客戶端 -> 代理1 -> 代理2 -> 源服務器
在源服務器收到請求後,會在請求頭拿到這個字段:
Via: proxy_server1, proxy_server2
而源服務器響應時,最終在客戶端會拿到這樣的響應頭:
Via: proxy_server2, proxy_server1
可以看到,Via中(zhōng)代理的順序即爲在 HTTP 傳輸中(zhōng)報文傳達的順序。
X-Forwarded-For
字面意思就是爲誰轉發, 它記錄的是請求方的IP地址(注意,和Via區分(fēn)開(kāi),X-Forwarded-For記錄的是請求方這一(yī)個IP)。
X-Real-IP
是一(yī)種獲取用戶真實 IP 的字段,不管中(zhōng)間經過多少代理,這個字段始終記錄最初的客戶端的IP。
相應的,還有X-Forwarded-Host和X-Forwarded-Proto,分(fēn)别記錄客戶端(注意哦,不包括代理)的域名和協議名。
X-Forwarded-For産生(shēng)的問題
前面可以看到,X-Forwarded-For這個字段記錄的是請求方的 IP,這意味着每經過一(yī)個不同的代理,這個字段的名字都要變,從客戶端到代理1,這個字段是客戶端的 IP,從代理1到代理2,這個字段就變爲了代理1的 IP。
但是這會産生(shēng)兩個問題:
1. 意味着代理必須解析 HTTP 請求頭,然後修改,比直接轉發數據性能下(xià)降。
2. 在 HTTPS 通信加密的過程中(zhōng),原始報文是不允許修改的。
由此産生(shēng)了代理協議,一(yī)般使用明文版本,隻需要在 HTTP 請求行上面加上這樣格式的文本即可:
// PROXY + TCP4/TCP6 + 請求方地址 + 接收方地址 + 請求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...
這樣就可以解決X-Forwarded-For帶來的問題了。
013: 如何理解 HTTP 緩存及緩存代理?
關于強緩存和協商(shāng)緩存的内容,我(wǒ)(wǒ)已經在能不能說一(yī)說浏覽器緩存做了詳細分(fēn)析,小(xiǎo)結如下(xià):
首先通過 Cache-Control 驗證強緩存是否可用
• 如果強緩存可用,直接使用
• 否則進入協商(shāng)緩存,即發送 HTTP 請求,服務器通過請求頭中(zhōng)的
If-Modified-Since
或者
If-None-Match
這些
條件請求
字段檢查資(zī)源是否更新
– 若資(zī)源更新,返回資(zī)源和200狀态碼
– 否則,返回304,告訴浏覽器直接從緩存獲取資(zī)源
這一(yī)節我(wǒ)(wǒ)們主要來說說另外(wài)一(yī)種緩存方式: 代理緩存。
爲什麽産生(shēng)代理緩存?
對于源服務器來說,它也是有緩存的,比如Redis, Memcache,但對于 HTTP 緩存來說,如果每次客戶端緩存失效都要到源服務器獲取,那給源服務器的壓力是很大(dà)的。
由此引入了緩存代理的機制。讓代理服務器接管一(yī)部分(fēn)的服務端HTTP緩存,客戶端緩存過期後就近到代理緩存中(zhōng)獲取,代理緩存過期了才請求源服務器,這樣流量巨大(dà)的時候能明顯降低源服務器的壓力。
那緩存代理究竟是如何做到的呢?
總的來說,緩存代理的控制分(fēn)爲兩部分(fēn),一(yī)部分(fēn)是源服務器端的控制,一(yī)部分(fēn)是客戶端的控制。
源服務器的緩存控制
private 和 public
在源服務器的響應頭中(zhōng),會加上Cache-Control這個字段進行緩存控制字段,那麽它的值當中(zhōng)可以加入private或者public表示是否允許代理服務器緩存,前者禁止,後者爲允許。
比如對于一(yī)些非常私密的數據,如果緩存到代理服務器,别人直接訪問代理就可以拿到這些數據,是非常危險的,因此對于這些數據一(yī)般是不會允許代理服務器進行緩存的,将響應頭部的Cache-Control設爲private,而不是public。
proxy-revalidate
must-revalidate的意思是客戶端緩存過期就去(qù)源服務器獲取,而proxy-revalidate則表示代理服務器的緩存過期後到源服務器獲取。
s-maxage
s是share的意思,限定了緩存在代理服務器中(zhōng)可以存放(fàng)多久,和限制客戶端緩存時間的max-age并不沖突。
講了這幾個字段,我(wǒ)(wǒ)們不妨來舉個小(xiǎo)例子,源服務器在響應頭中(zhōng)加入這樣一(yī)個字段:
Cache-Control: public, max-age=1000, s-maxage=2000
相當于源服務器說: 我(wǒ)(wǒ)這個響應是允許代理服務器緩存的,客戶端緩存過期了到代理中(zhōng)拿,并且在客戶端的緩存時間爲 1000 秒,在代理服務器中(zhōng)的緩存時間爲 2000 s。
客戶端的緩存控制
max-stale 和 min-fresh
在客戶端的請求頭中(zhōng),可以加入這兩個字段,來對代理服務器上的緩存進行寬容和限制操作。比如:
max-stale: 5
表示客戶端到代理服務器上拿緩存的時候,即使代理緩存過期了也不要緊,隻要過期時間在5秒之内,還是可以從代理中(zhōng)獲取的。
又(yòu)比如:
min-fresh: 5
表示代理緩存需要一(yī)定的新鮮度,不要等到緩存剛好到期再拿,一(yī)定要在到期前 5 秒之前的時間拿,否則拿不到。
only-if-cached
這個字段加上後表示客戶端隻會接受代理緩存,而不會接受源服務器的響應。如果代理緩存無效,則直接返回504(Gateway Timeout)。
以上便是緩存代理的内容,涉及的字段比較多,希望能好好回顧一(yī)下(xià),加深理解。
014: 什麽是跨域?浏覽器如何攔截響應?如何解決?
在前後端分(fēn)離(lí)的開(kāi)發模式中(zhōng),經常會遇到跨域問題,即 Ajax 請求發出去(qù)了,服務器也成功響應了,前端就是拿不到這個響應。接下(xià)來我(wǒ)(wǒ)們就來好好讨論一(yī)下(xià)這個問題。
什麽是跨域
浏覽器遵循同源政策(scheme(協議)、host(主機)和port(端口)都相同則爲同源)。非同源站點有這樣一(yī)些限制:
• 不能讀取和修改對方的 DOM
• 不讀訪問對方的 Cookie、IndexDB 和 LocalStorage
• 限制 XMLHttpRequest 請求。(後面的話(huà)題着重圍繞這個)
當浏覽器向目标 URI 發 Ajax 請求時,隻要當前 URL 和目标 URL 不同源,則産生(shēng)跨域,被稱爲跨域請求。
跨域請求的響應一(yī)般會被浏覽器所攔截,注意,是被浏覽器攔截,響應其實是成功到達客戶端了。那這個攔截是如何發生(shēng)呢?
首先要知(zhī)道的是,浏覽器是多進程的,以 Chrome 爲例,進程組成如下(xià):
WebKit 渲染引擎和V8 引擎都在渲染進程當中(zhōng)。
當xhr.send被調用,即 Ajax 請求準備發送的時候,其實還隻是在渲染進程的處理。爲了防止黑客通過腳本觸碰到系統資(zī)源,浏覽器将每一(yī)個渲染進程裝進了沙箱,并且爲了防止 CPU 芯片一(yī)直存在的Spectre 和 Meltdown漏洞,采取了站點隔離(lí)的手段,給每一(yī)個不同的站點(一(yī)級域名不同)分(fēn)配了沙箱,互不幹擾。具體(tǐ)見YouTube上Chromium安全團隊的演講視頻(pín)。
在沙箱當中(zhōng)的渲染進程是沒有辦法發送網絡請求的,那怎麽辦?隻能通過網絡進程來發送。那這樣就涉及到進程間通信(IPC,Inter Process Communication)了。接下(xià)來我(wǒ)(wǒ)們看看 chromium 當中(zhōng)進程間通信是如何完成的,在 chromium 源碼中(zhōng)調用順序如下(xià):
可能看了你會比較懵,如果想深入了解可以去(qù)看看 chromium 最新的源代碼,IPC源碼地址及Chromium IPC源碼解析文章。
總的來說就是利用Unix Domain Socket套接字,配合事件驅動的高性能網絡并發庫libevent完成進程的 IPC 過程。
好,現在數據傳遞給了浏覽器主進程,主進程接收到後,才真正地發出相應的網絡請求。
在服務端處理完數據後,将響應返回,主進程檢查到跨域,且沒有cors(後面會詳細說)響應頭,将響應體(tǐ)全部丢掉,并不會發送給渲染進程。這就達到了攔截數據的目的。
接下(xià)來我(wǒ)(wǒ)們來說一(yī)說解決跨域問題的幾種方案。
CORS
CORS 其實是 W3C 的一(yī)個标準,全稱是跨域資(zī)源共享。它需要浏覽器和服務器的共同支持,具體(tǐ)來說,非 IE 和 IE10 以上支持CORS,服務器需要附加特定的響應頭,後面具體(tǐ)拆解。不過在弄清楚 CORS 的原理之前,我(wǒ)(wǒ)們需要清楚兩個概念: 簡單請求和非簡單請求。
浏覽器根據請求方法和請求頭的特定字段,将請求做了一(yī)下(xià)分(fēn)類,具體(tǐ)來說規則是這樣,凡是滿足下(xià)面條件的屬于簡單請求:
• 請求方法爲 GET、POST 或者 HEAD
• 請求頭的取值範圍: Accept、Accept-Language、Content-Language、Content-Type(隻限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain)
浏覽器畫了這樣一(yī)個圈,在這個圈裏面的就是簡單請求, 圈外(wài)面的就是非簡單請求,然後針對這兩種不同的請求進行不同的處理。
簡單請求
請求發出去(qù)之前,浏覽器做了什麽?
它會自動在請求頭當中(zhōng),添加一(yī)個Origin字段,用來說明請求來自哪個源。服務器拿到請求之後,在回應時對應地添加Access-Control-Allow-Origin字段,如果Origin不在這個字段的範圍中(zhōng),那麽浏覽器就會将響應攔截。
因此,Access-Control-Allow-Origin字段是服務器用來決定浏覽器是否攔截這個響應,這是必需的字段。與此同時,其它一(yī)些可選的功能性的字段,用來描述如果不會攔截,這些字段将會發揮各自的作用。
Access-Control-Allow-Credentials。這個字段是一(yī)個布爾值,表示是否允許發送 Cookie,對于跨域請求,浏覽器對這個字段默認值設爲 false,而如果需要拿到浏覽器的 Cookie,需要添加這個響應頭并設爲true, 并且在前端也需要設置withCredentials屬性:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
Access-Control-Expose-Headers。這個字段是給 XMLHttpRequest 對象賦能,讓它不僅可以拿到基本的 6 個響應頭字段(包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma), 還能拿到這個字段聲明的響應頭字段。比如這樣設置:
Access-Control-Expose-Headers: aaa
那麽在前端可以通過 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 這個字段的值。
非簡單請求
非簡單請求相對而言會有些不同,體(tǐ)現在兩個方面: 預檢請求和響應字段。
我(wǒ)(wǒ)們以 PUT 方法爲例。
var url = 'http://xxx.com';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'xxx');
xhr.send();
當這段代碼執行後,首先會發送預檢請求。這個預檢請求的請求行和請求體(tǐ)是下(xià)面這個格式:
OPTIONS / HTTP/1.1
Origin: 當前地址
Host: xxx.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
預檢請求的方法是OPTIONS,同時會加上Origin源地址和Host目标地址,這很簡單。同時也會加上兩個關鍵的字段:
• Access-Control-Request-Method, 列出 CORS 請求用到哪個HTTP方法
• Access-Control-Request-Headers,指定 CORS 請求将要加上什麽請求頭
這是預檢請求。接下(xià)來是響應字段,響應字段也分(fēn)爲兩部分(fēn),一(yī)部分(fēn)是對于預檢請求的響應,一(yī)部分(fēn)是對于 CORS 請求的響應。
預檢請求的響應。如下(xià)面的格式:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
其中(zhōng)有這樣幾個關鍵的響應頭字段:
• Access-Control-Allow-Origin: 表示可以允許請求的源,可以填具體(tǐ)的源名,也可以填*表示允許任意源請求。
• Access-Control-Allow-Methods: 表示允許的請求方法列表。
• Access-Control-Allow-Credentials: 簡單請求中(zhōng)已經介紹。
• Access-Control-Allow-Headers: 表示允許發送的請求頭字段
• Access-Control-Max-Age: 預檢請求的有效期,在此期間,不用發出另外(wài)一(yī)條預檢請求。
在預檢請求的響應返回後,如果請求不滿足響應頭的條件,則觸發XMLHttpRequest的onerror方法,當然後面真正的CORS請求也不會發出去(qù)了。
CORS 請求的響應。繞了這麽一(yī)大(dà)轉,到了真正的 CORS 請求就容易多了,現在它和簡單請求的情況是一(yī)樣的。浏覽器自動加上Origin字段,服務端響應頭返回Access-Control-Allow-Origin。可以參考以上簡單請求部分(fēn)的内容。
JSONP
雖然XMLHttpRequest對象遵循同源政策,但是script标簽不一(yī)樣,它可以通過 src 填上目标地址從而發出 GET 請求,實現跨域請求并拿到響應。這也就是 JSONP 的原理,接下(xià)來我(wǒ)(wǒ)們就來封裝一(yī)個 JSONP:
const jsonp = ({ url, params, callbackName }) => {
const generateURL = () => {
let dataStr = '';
for(let key in params) {
dataStr += `${key}=${params[key]}&`;
}
dataStr += `callback=${callbackName}`;
return `${url}?${dataStr}`;
};
return new Promise((resolve, reject) => {
// 初始化回調函數名稱
callbackName = callbackName || Math.random().toString.replace(',', '');
// 創建 script 元素并加入到當前文檔中(zhōng)
let scriptEle = document.createElement('script');
scriptEle.src = generateURL();
document.body.appendChild(scriptEle);
// 綁定到 window 上,爲了後面調用
window[callbackName] = (data) => {
resolve(data);
// script 執行完了,成爲無用元素,需要清除
document.body.removeChild(scriptEle);
}
});
}
當然在服務端也會有響應的操作, 以 express 爲例:
let express = require('express')
let app = express()
app.get('/', function(req, res) {
let { a, b, callback } = req.query
console.log(a); // 1
console.log(b); // 2
// 注意哦,返回給script标簽,浏覽器直接把這部分(fēn)字符串執行
res.end(`${callback}('數據包')`);
})
app.listen(3000)
前端這樣簡單地調用一(yī)下(xià)就好了:
jsonp({
url: 'http://localhost:3000',
params: {
a: 1,
b: 2
}
}).then(data => {
// 拿到數據進行處理
console.log(data); // 數據包
})
和CORS相比,JSONP 最大(dà)的優勢在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺點也很明顯,請求方法單一(yī),隻支持 GET 請求。
Nginx
Nginx 是一(yī)種高性能的反向代理服務器,可以用來輕松解決跨域問題。
正向代理幫助客戶端訪問客戶端自己訪問不到的服務器,然後将結果返回給客戶端。
反向代理拿到客戶端的請求,将請求轉發給其他的服務器,主要的場景是維持服務器集群的負載均衡,換句話(huà)說,反向代理幫其它的服務器拿到請求,然後選擇一(yī)個合适的服務器,将請求轉交給它。
因此,兩者的區别就很明顯了,正向代理服務器是幫客戶端做事情,而反向代理服務器是幫其它的服務器做事情。
好了,那 Nginx 是如何來解決跨域的呢?
比如說現在客戶端的域名爲client.com,服務器的域名爲server.com,客戶端向服務器發送 Ajax 請求,當然會跨域了,那這個時候讓 Nginx 登場了,通過下(xià)面這個配置:
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
Nginx 相當于起了一(yī)個跳闆機,這個跳闆機的域名也是client.com,讓客戶端首先訪問 client.com/api,這當然沒有跨域,然後 Nginx 服務器作爲反向代理,将請求轉發給server.com,當響應返回時又(yòu)将響應給到客戶端,這就完成整個跨域請求的過程。
其實還有一(yī)些不太常用的方式,大(dà)家了解即可,比如postMessage,當然WebSocket也是一(yī)種方式,但是已經不屬于 HTTP 的範疇,另外(wài)一(yī)些奇技淫巧就不建議大(dà)家去(qù)死記硬背了,一(yī)方面從來不用,名字都難得記住,另一(yī)方面臨時背下(xià)來,面試官也不會對你印象加分(fēn),因爲看得出來是背的。當然沒有背并不代表減分(fēn),把跨域原理和前面三種主要的跨域方式理解清楚,經得起更深一(yī)步的推敲,反而會讓别人覺得你是一(yī)個靠譜的人。
015: TLS1.2 握手的過程是怎樣的?
之前談到了 HTTP 是明文傳輸的協議,傳輸保文對外(wài)完全透明,非常不安全,那如何進一(yī)步保證安全性呢?
由此産生(shēng)了 HTTPS,其實它并不是一(yī)個新的協議,而是在 HTTP 下(xià)面增加了一(yī)層 SSL/TLS 協議,簡單的講,HTTPS = HTTP + SSL/TLS。
那什麽是 SSL/TLS 呢?
SSL 即安全套接層(Secure Sockets Layer),在 OSI 七層模型中(zhōng)處于會話(huà)層(第 5 層)。之前 SSL 出過三個大(dà)版本,當它發展到第三個大(dà)版本的時候才被标準化,成爲 TLS(傳輸層安全,Transport Layer Security),并被當做 TLS1.0 的版本,準确地說,TLS1.0 = SSL3.1。
現在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被認爲是不安全的,在不久的将來會被完全淘汰。因此我(wǒ)(wǒ)們接下(xià)來主要讨論的是 TLS1.2, 當然在 2018 年推出了更加優秀的 TLS1.3,大(dà)大(dà)優化了 TLS 握手過程,這個我(wǒ)(wǒ)們放(fàng)在下(xià)一(yī)節再去(qù)說。
傳統 RSA 握手
先來說說傳統的 TLS 握手,也是大(dà)家在網上經常看到的。我(wǒ)(wǒ)之前也寫過這樣的文章,(傳統RSA版本)HTTPS爲什麽讓數據傳輸更安全,其中(zhōng)也介紹到了對稱加密和非對稱加密的概念,建議大(dà)家去(qù)讀一(yī)讀,不再贅述。之所以稱它爲 RSA 版本,是因爲它在加解密pre_random的時候采用的是 RSA 算法。
TLS 1.2 握手過程
現在我(wǒ)(wǒ)們來講講主流的 TLS 1.2 版本所采用的方式。
剛開(kāi)始你可能會比較懵,先别着急,過一(yī)遍下(xià)面的流程再來看會豁然開(kāi)朗。
step 1: Client Hello
首先,浏覽器發送 client_random、TLS版本、加密套件列表。
client_random 是什麽?用來最終 secret 的一(yī)個參數。
加密套件列表是什麽?我(wǒ)(wǒ)舉個例子,加密套件列表一(yī)般張這樣:
TLS_ECDHE_WITH_AES_128_GCM_SHA256
意思是TLS握手過程中(zhōng),使用ECDHE算法生(shēng)成pre_random(這個數後面會介紹),128位的AES算法進行對稱加密,在對稱加密的過程中(zhōng)使用主流的GCM分(fēn)組模式,因爲對稱加密中(zhōng)很重要的一(yī)個問題就是如何分(fēn)組。最後一(yī)個是哈希摘要算法,采用SHA256算法。
其中(zhōng)值得解釋一(yī)下(xià)的是這個哈希摘要算法,試想一(yī)個這樣的場景,服務端現在給客戶端發消息來了,客戶端并不知(zhī)道此時的消息到底是服務端發的,還是中(zhōng)間人僞造的消息呢?現在引入這個哈希摘要算法,将服務端的證書(shū)信息通過這個算法生(shēng)成一(yī)個摘要(可以理解爲比較短的字符串),用來标識這個服務端的身份,用私鑰加密後把加密後的标識和自己的公鑰傳給客戶端。客戶端拿到這個公鑰來解密,生(shēng)成另外(wài)一(yī)份摘要。兩個摘要進行對比,如果相同則能确認服務端的身份。這也就是所謂數字簽名的原理。其中(zhōng)除了哈希算法,最重要的過程是私鑰加密,公鑰解密。
step 2: Server Hello
可以看到服務器一(yī)口氣給客戶端回複了非常多的内容。
server_random也是最後生(shēng)成secret的一(yī)個參數, 同時确認 TLS 版本、需要使用的加密套件和自己的證書(shū),這都不難理解。那剩下(xià)的server_params是幹嘛的呢?
我(wǒ)(wǒ)們先埋個伏筆,現在你隻需要知(zhī)道,server_random到達了客戶端。
step 3: Client 驗證證書(shū),生(shēng)成secret
客戶端驗證服務端傳來的證書(shū)和簽名是否通過,如果驗證通過,則傳遞client_params這個參數給服務器。
接着客戶端通過ECDHE算法計算出pre_random,其中(zhōng)傳入兩個參數:server_params和client_params。現在你應該清楚這個兩個參數的作用了吧,由于ECDHE基于橢圓曲線離(lí)散對數,這兩個參數也稱作橢圓曲線的公鑰。
客戶端現在擁有了client_random、server_random和pre_random,接下(xià)來将這三個數通過一(yī)個僞随機數函數來計算出最終的secret。
step4: Server 生(shēng)成 secret
剛剛客戶端不是傳了client_params過來了嗎(ma)?
現在服務端開(kāi)始用ECDHE算法生(shēng)成pre_random,接着用和客戶端同樣的僞随機數函數生(shēng)成最後的secret。
注意事項
TLS的過程基本上講完了,但還有兩點需要注意。
第一(yī)、實際上 TLS 握手是一(yī)個雙向認證的過程,從 step1 中(zhōng)可以看到,客戶端有能力驗證服務器的身份,那服務器能不能驗證客戶端的身份呢?
當然是可以的。具體(tǐ)來說,在 step3中(zhōng),客戶端傳送client_params,實際上給服務器傳一(yī)個驗證消息,讓服務器将相同的驗證流程(哈希摘要 + 私鑰加密 + 公鑰解密)走一(yī)遍,确認客戶端的身份。
第二、當客戶端生(shēng)成secret後,會給服務端發送一(yī)個收尾的消息,告訴服務器之後的都用對稱加密,對稱加密的算法就用第一(yī)次約定的。服務器生(shēng)成完secret也會向客戶端發送一(yī)個收尾的消息,告訴客戶端以後就直接用對稱加密來通信。
這個收尾的消息包括兩部分(fēn),一(yī)部分(fēn)是Change Cipher Spec,意味着後面加密傳輸了,另一(yī)個是Finished消息,這個消息是對之前所有發送的數據做的摘要,對摘要進行加密,讓對方驗證一(yī)下(xià)。
當雙方都驗證通過之後,握手才正式結束。後面的 HTTP 正式開(kāi)始傳輸加密報文。
RSA 和 ECDHE 握手過程的區别
1. ECDHE 握手,也就是主流的 TLS1.2 握手中(zhōng),使用ECDHE實現pre_random的加密解密,沒有用到 RSA。
2. 使用 ECDHE 還有一(yī)個特點,就是客戶端發送完收尾消息後可以提前搶跑,直接發送 HTTP 報文,節省了一(yī)個 RTT,不必等到收尾消息到達服務器,然後等服務器返回收尾消息給自己,直接開(kāi)始發請求。這也叫TLS False Start。
016: TLS 1.3 做了哪些改進?
TLS 1.2 雖然存在了 10 多年,經曆了無數的考驗,但曆史的車(chē)輪總是不斷向前的,爲了獲得更強的安全、更優秀的性能,在2018年就推出了 TLS1.3,對于TLS1.2做了一(yī)系列的改進,主要分(fēn)爲這幾個部分(fēn):強化安全、提高性能。
強化安全
在 TLS1.3 中(zhōng)廢除了非常多的加密算法,最後隻保留五個加密套件:
• TLS_AES_128_GCM_SHA256
• TLS_AES_256_GCM_SHA384
• TLS_CHACHA20_POLY1305_SHA256
• TLS_AES_128_GCM_SHA256
• TLS_AES_128_GCM_8_SHA256
可以看到,最後剩下(xià)的對稱加密算法隻有 AES 和 CHACHA20,之前主流的也會這兩種。分(fēn)組模式也隻剩下(xià) GCM 和 POLY1305, 哈希摘要算法隻剩下(xià)了 SHA256 和 SHA384 了。
那你可能會問了, 之前RSA這麽重要的非對稱加密算法怎麽不在了?
我(wǒ)(wǒ)覺得有兩方面的原因:
第一(yī)、2015年發現了FREAK攻擊,即已經有人發現了 RSA 的漏洞,能夠進行破解了。
第二、一(yī)旦私鑰洩露,那麽中(zhōng)間人可以通過私鑰計算出之前所有報文的secret,破解之前所有的密文。
爲什麽?回到 RSA 握手的過程中(zhōng),客戶端拿到服務器的證書(shū)後,提取出服務器的公鑰,然後生(shēng)成pre_random并用公鑰加密傳給服務器,服務器通過私鑰解密,從而拿到真實的pre_random。當中(zhōng)間人拿到了服務器私鑰,并且截獲之前所有報文的時候,那麽就能拿到pre_random、server_random和client_random并根據對應的随機數函數生(shēng)成secret,也就是拿到了 TLS 最終的會話(huà)密鑰,每一(yī)個曆史報文都能通過這樣的方式進行破解。
但ECDHE在每次握手時都會生(shēng)成臨時的密鑰對,即使私鑰被破解,之前的曆史消息并不會收到影響。這種一(yī)次破解并不影響曆史信息的性質也叫前向安全性。
RSA 算法不具備前向安全性,而 ECDHE 具備,因此在 TLS1.3 中(zhōng)徹底取代了RSA。
提升性能
握手改進
流程如下(xià):
大(dà)體(tǐ)的方式和 TLS1.2 差不多,不過和 TLS 1.2 相比少了一(yī)個 RTT, 服務端不必等待對方驗證證書(shū)之後才拿到client_params,而是直接在第一(yī)次握手的時候就能夠拿到, 拿到之後立即計算secret,節省了之前不必要的等待時間。同時,這也意味着在第一(yī)次握手的時候客戶端需要傳送更多的信息,一(yī)口氣給傳完。
這種 TLS 1.3 握手方式也被叫做1-RTT握手。但其實這種1-RTT的握手方式還是有一(yī)些優化的空間的,接下(xià)來我(wǒ)(wǒ)們來一(yī)一(yī)介紹這些優化方式。
會話(huà)複用
會話(huà)複用有兩種方式: Session ID和Session Ticket。
先說說最早出現的Seesion ID,具體(tǐ)做法是客戶端和服務器首次連接後各自保存會話(huà)的 ID,并存儲會話(huà)密鑰,當再次連接時,客戶端發送ID過來,服務器查找這個 ID 是否存在,如果找到了就直接複用之前的會話(huà)狀态,會話(huà)密鑰不用重新生(shēng)成,直接用原來的那份。
但這種方式也存在一(yī)個弊端,就是當客戶端數量龐大(dà)的時候,對服務端的存儲壓力非常大(dà)。
因而出現了第二種方式——Session Ticket。它的思路就是: 服務端的壓力大(dà),那就把壓力分(fēn)攤給客戶端呗。具體(tǐ)來說,雙方連接成功後,服務器加密會話(huà)信息,用Session Ticket消息發給客戶端,讓客戶端保存下(xià)來。下(xià)次重連的時候,就把這個 Ticket 進行解密,驗證它過沒過期,如果沒過期那就直接恢複之前的會話(huà)狀态。
這種方式雖然減小(xiǎo)了服務端的存儲壓力,但與帶來了安全問題,即每次用一(yī)個固定的密鑰來解密 Ticket 數據,一(yī)旦黑客拿到這個密鑰,之前所有的曆史記錄也被破解了。因此爲了盡量避免這樣的問題,密鑰需要定期進行更換。
總的來說,這些會話(huà)複用的技術在保證1-RTT的同時,也節省了生(shēng)成會話(huà)密鑰這些算法所消耗的時間,是一(yī)筆可觀的性能提升。
PSK
剛剛說的都是1-RTT情況下(xià)的優化,那能不能優化到0-RTT呢?
答案是可以的。做法其實也很簡單,在發送Session Ticket的同時帶上應用數據,不用等到服務端确認,這種方式被稱爲Pre-Shared Key,即 PSK。
這種方式雖然方便,但也帶來了安全問題。中(zhōng)間人截獲PSK的數據,不斷向服務器重複發,類似于 TCP 第一(yī)次握手攜帶數據,增加了服務器被攻擊的風險。
總結
TLS1.3 在 TLS1.2 的基礎上廢除了大(dà)量的算法,提升了安全性。同時利用會話(huà)複用節省了重新生(shēng)成密鑰的時間,利用 PSK 做到了0-RTT連接。
017: HTTP/2 有哪些改進?
由于 HTTPS 在安全方面已經做的非常好了,HTTP 改進的關注點放(fàng)在了性能方面。對于 HTTP/2 而言,它對于性能的提升主要在于兩點:
• 頭部壓縮
• 多路複用
當然還有一(yī)些颠覆性的功能實現:
• 設置請求優先級
• 服務器推送
這些重大(dà)的提升本質上也是爲了解決 HTTP 本身的問題而産生(shēng)的。接下(xià)來我(wǒ)(wǒ)們來看看 HTTP/2 解決了哪些問題,以及解決方式具體(tǐ)是如何的。
頭部壓縮
在 HTTP/1.1 及之前的時代,請求體(tǐ)一(yī)般會有響應的壓縮編碼過程,通過Content-Encoding頭部字段來指定,但你有沒有想過頭部字段本身的壓縮呢?當請求字段非常複雜(zá)的時候,尤其對于 GET 請求,請求報文幾乎全是請求頭,這個時候還是存在非常大(dà)的優化空間的。HTTP/2 針對頭部字段,也采用了對應的壓縮算法——HPACK,對請求頭進行壓縮。
HPACK 算法是專門爲 HTTP/2 服務的,它主要的亮點有兩個:
• 首先是在服務器和客戶端之間建立哈希表,将用到的字段存放(fàng)在這張表中(zhōng),那麽在傳輸的時候對于之前出現過的值,隻需要把索引(比如0,1,2,...)傳給對方即可,對方拿到索引查表就行了。這種傳索引的方式,可以說讓請求頭字段得到極大(dà)程度的精簡和複用。
HTTP/2 當中(zhōng)廢除了起始行的概念,将起始行中(zhōng)的請求方法、URI、狀态碼轉換成了頭字段,不過這些字段都有一(yī)個":"前綴,用來和其它請求頭區分(fēn)開(kāi)。
• 其次是對于整數和字符串進行哈夫曼編碼,哈夫曼編碼的原理就是先将所有出現的字符建立一(yī)張索引表,然後讓出現次數多的字符對應的索引盡可能短,傳輸的時候也是傳輸這樣的索引序列,可以達到非常高的壓縮率。
多路複用
HTTP 隊頭阻塞
我(wǒ)(wǒ)們之前讨論了 HTTP 隊頭阻塞的問題,其根本原因在于HTTP 基于請求-響應的模型,在同一(yī)個 TCP 長連接中(zhōng),前面的請求沒有得到響應,後面的請求就會被阻塞。
後面我(wǒ)(wǒ)們又(yòu)讨論到用并發連接和域名分(fēn)片的方式來解決這個問題,但這并沒有真正從 HTTP 本身的層面解決問題,隻是增加了 TCP 連接,分(fēn)攤風險而已。而且這麽做也有弊端,多條 TCP 連接會競争有限的帶寬,讓真正優先級高的請求不能優先處理。
而 HTTP/2 便從 HTTP 協議本身解決了隊頭阻塞問題。注意,這裏并不是指的TCP隊頭阻塞,而是HTTP隊頭阻塞,兩者并不是一(yī)回事。TCP 的隊頭阻塞是在數據包層面,單位是數據包,前一(yī)個報文沒有收到便不會将後面收到的報文上傳給 HTTP,而HTTP 的隊頭阻塞是在 HTTP 請求-響應層面,前一(yī)個請求沒處理完,後面的請求就要阻塞住。兩者所在的層次不一(yī)樣。
那麽 HTTP/2 如何來解決所謂的隊頭阻塞呢?
二進制分(fēn)幀
首先,HTTP/2 認爲明文傳輸對機器而言太麻煩了,不方便計算機的解析,因爲對于文本而言會有多義性的字符,比如回車(chē)換行到底是内容還是分(fēn)隔符,在内部需要用到狀态機去(qù)識别,效率比較低。于是 HTTP/2 幹脆把報文全部換成二進制格式,全部傳輸01串,方便了機器的解析。
原來Headers + Body的報文格式如今被拆分(fēn)成了一(yī)個個二進制的幀,用Headers幀存放(fàng)頭部字段,Data幀存放(fàng)請求體(tǐ)數據。分(fēn)幀之後,服務器看到的不再是一(yī)個個完整的 HTTP 請求報文,而是一(yī)堆亂序的二進制幀。這些二進制幀不存在先後關系,因此也就不會排隊等待,也就沒有了 HTTP 的隊頭阻塞問題。
通信雙方都可以給對方發送二進制幀,這種二進制幀的雙向傳輸的序列,也叫做流(Stream)。HTTP/2 用流來在一(yī)個 TCP 連接上來進行多個數據幀的通信,這就是多路複用的概念。
可能你會有一(yī)個疑問,既然是亂序首發,那最後如何來處理這些亂序的數據幀呢?
首先要聲明的是,所謂的亂序,指的是不同 ID 的 Stream 是亂序的,但同一(yī)個 Stream ID 的幀一(yī)定是按順序傳輸的。二進制幀到達後對方會将 Stream ID 相同的二進制幀組裝成完整的請求報文和響應報文。當然,在二進制幀當中(zhōng)還有其他的一(yī)些字段,實現了優先級和流量控制等功能,我(wǒ)(wǒ)們放(fàng)到下(xià)一(yī)節再來介紹。
服務器推送
另外(wài)值得一(yī)說的是 HTTP/2 的服務器推送(Server Push)。在 HTTP/2 當中(zhōng),服務器已經不再是完全被動地接收請求,響應請求,它也能新建 stream 來給客戶端發送消息,當 TCP 連接建立之後,比如浏覽器請求一(yī)個 HTML 文件,服務器就可以在返回 HTML 的基礎上,将 HTML 中(zhōng)引用到的其他資(zī)源文件一(yī)起返回給客戶端,減少客戶端的等待。
總結
當然,HTTP/2 新增那麽多的特性,是不是 HTTP 的語法要重新學呢?不需要,HTTP/2 完全兼容之前 HTTP 的語法和語義,如請求頭、URI、狀态碼、頭部字段都沒有改變,完全不用擔心。同時,在安全方面,HTTP 也支持 TLS,并且現在主流的浏覽器都公開(kāi)隻支持加密的 HTTP/2, 因此你現在能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。
018: HTTP/2 中(zhōng)的二進制幀是如何設計的?
幀結構
HTTP/2 中(zhōng)傳輸的幀結構如下(xià)圖所示:
每個幀分(fēn)爲幀頭和幀體(tǐ)。先是三個字節的幀長度,這個長度表示的是幀體(tǐ)的長度。
然後是幀類型,大(dà)概可以分(fēn)爲數據幀和控制幀兩種。數據幀用來存放(fàng) HTTP 報文,控制幀用來管理流的傳輸。
接下(xià)來的一(yī)個字節是幀标志(zhì),裏面一(yī)共有 8 個标志(zhì)位,常用的有 END_HEADERS表示頭數據結束,END_STREAM表示單方向數據發送結束。
後 4 個字節是Stream ID, 也就是流标識符,有了它,接收方就能從亂序的二進制幀中(zhōng)選擇出 ID 相同的幀,按順序組裝成請求/響應報文。
流的狀态變化
從前面可以知(zhī)道,在 HTTP/2 中(zhōng),所謂的流,其實就是二進制幀的雙向傳輸的序列。那麽在 HTTP/2 請求和響應的過程中(zhōng),流的狀态是如何變化的呢?
HTTP/2 其實也是借鑒了 TCP 狀态變化的思想,根據幀的标志(zhì)位來實現具體(tǐ)的狀态改變。這裏我(wǒ)(wǒ)們以一(yī)個普通的請求-響應過程爲例來說明:
最開(kāi)始兩者都是空閑狀态,當客戶端發送Headers幀後,開(kāi)始分(fēn)配Stream ID, 此時客戶端的流打開(kāi), 服務端接收之後服務端的流也打開(kāi),兩端的流都打開(kāi)之後,就可以互相傳遞數據幀和控制幀了。
當客戶端要關閉時,向服務端發送END_STREAM幀,進入半關閉狀态, 這個時候客戶端隻能接收數據,而不能發送數據。
服務端收到這個END_STREAM幀後也進入半關閉狀态,不過此時服務端的情況是隻能發送數據,而不能接收數據。随後服務端也向客戶端發送END_STREAM幀,表示數據發送完畢,雙方進入關閉狀态。
如果下(xià)次要開(kāi)啓新的流,流 ID 需要自增,直到上限爲止,到達上限後開(kāi)一(yī)個新的 TCP 連接重頭開(kāi)始計數。由于流 ID 字段長度爲 4 個字節,最高位又(yòu)被保留,因此範圍是 0 ~ 2的 31 次方,大(dà)約 21 億個。
流的特性
剛剛談到了流的狀态變化過程,這裏順便就來總結一(yī)下(xià)流傳輸的特性:
• 并發性。一(yī)個 HTTP/2 連接上可以同時發多個幀,這一(yī)點和 HTTP/1 不同。這也是實現多路複用的基礎。
• 自增性。流 ID 是不可重用的,而是會按順序遞增,達到上限之後又(yòu)新開(kāi) TCP 連接從頭開(kāi)始。
• 雙向性。客戶端和服務端都可以創建流,互不幹擾,雙方都可以作爲發送方或者接收方。
• 可設置優先級。可以設置數據幀的優先級,讓服務端先處理重要資(zī)源,優化用戶體(tǐ)驗。
掃二維碼與項目經理溝通
我(wǒ)(wǒ)們在微信上24小(xiǎo)時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯網交流