HTTP 緩存別再亂用了!推薦一個(gè)緩存設(shè)置的最佳姿勢(shì)!
設(shè)置緩存大家可能大家都是從性能角度去考慮的,但是如果你不注意或者稍微設(shè)置不當(dāng),緩存也可能對(duì)我們的網(wǎng)站的安全性和用戶隱私造成負(fù)面影響。
開門見山
老規(guī)矩,先把推薦的配置說(shuō)出來(lái),后面再啰嗦:
- 為了防止中介緩存,建議設(shè)置:Cache-Control: private
- 建議設(shè)置適當(dāng)?shù)亩?jí)緩存 key:如果我們請(qǐng)求的響應(yīng)是跟請(qǐng)求的 Cookie 相關(guān)的,建議設(shè)置:Vary: Cookie
那么為啥推薦這兩個(gè)配置呢?如果不配置會(huì)對(duì)我們的網(wǎng)站帶來(lái)什么風(fēng)險(xiǎn)呢?且聽我下面的講解。
回顧 HTTP 緩存
提到緩存,大家可能很快就會(huì)想到兩種緩存方式,以及對(duì)應(yīng)的幾個(gè)請(qǐng)求頭,我們來(lái)快速回顧一下。
正常情況下,我們的瀏覽器客戶端會(huì)像服務(wù)器發(fā)起請(qǐng)求,然后服務(wù)器會(huì)將數(shù)據(jù)響應(yīng)返回給客戶端。

但是一臺(tái)服務(wù)器可能要對(duì)成千上萬(wàn)臺(tái)客戶端的請(qǐng)求進(jìn)行響應(yīng),其中也有非常多是重復(fù)的請(qǐng)求,這會(huì)對(duì)服務(wù)器造成非常大的壓力。


所以一般我們都會(huì)在客戶端和服務(wù)器間進(jìn)行一些緩存,對(duì)于一些重復(fù)的請(qǐng)求數(shù)據(jù),如果之前的響應(yīng)已經(jīng)被存儲(chǔ)到緩存數(shù)據(jù)庫(kù)中,滿足一定條件的話就會(huì)直接去緩存中取,不會(huì)到達(dá)服務(wù)器。


那么,HTTP 緩存一般又分為兩種,強(qiáng)緩存和協(xié)商緩存:
強(qiáng)緩存
強(qiáng)緩存,在緩存數(shù)據(jù)未失效的情況下,客戶端可以直接使用緩存數(shù)據(jù),不用和數(shù)據(jù)庫(kù)進(jìn)行交互。
那么,判斷請(qǐng)求是否失效主要靠?jī)蓚€(gè) HTTP Header:
- Expires:數(shù)據(jù)的緩存到期時(shí)間,下一次請(qǐng)求時(shí),請(qǐng)求時(shí)間小于服務(wù)端返回的到期時(shí)間,直接使用緩存數(shù)據(jù)。
- Cache-Control:可以指定一個(gè) max-age 字段,表示緩存的內(nèi)容將在一定時(shí)間后失效。
協(xié)商緩存
協(xié)商緩存,顧名思義需要和服務(wù)器進(jìn)行一次協(xié)商。瀏覽器第一次請(qǐng)求時(shí),服務(wù)器會(huì)將緩存標(biāo)識(shí)與數(shù)據(jù)一起返回給客戶端,客戶端將二者備份至緩存數(shù)據(jù)庫(kù)中。
再次請(qǐng)求數(shù)據(jù)時(shí),客戶端將備份的緩存標(biāo)識(shí)發(fā)送給服務(wù)器,服務(wù)器根據(jù)緩存標(biāo)識(shí)進(jìn)行判斷,判斷成功后,返回 304 狀態(tài)碼,通知客戶端比較成功,可以使用緩存數(shù)據(jù)。
判斷請(qǐng)求主要靠下面兩組 HTTP Header:
- Last-Modified:一個(gè) Response Header,服務(wù)器在響應(yīng)請(qǐng)求時(shí),告訴瀏覽器資源的最后修改時(shí)間。
- if-Modified-Since:一個(gè) Request Header,再次請(qǐng)求服務(wù)器時(shí),通過(guò)此字段通知服務(wù)器上次請(qǐng)求時(shí),服務(wù)器返回的資源最后修改時(shí)間。
服務(wù)器會(huì)通過(guò)收到的 If-Modified-Since 和資源的最后修改時(shí)間進(jìn)行比對(duì),判斷是否使用緩存。
- Etag:一個(gè) Response Header,服務(wù)器返回的資源的唯一標(biāo)示
- If-None-Match:一個(gè) Request Header,再次請(qǐng)求服務(wù)器時(shí),通過(guò)此字段通知服務(wù)器客戶段緩存數(shù)據(jù)的唯一標(biāo)識(shí)。
服務(wù)器會(huì)通過(guò)收到的 If-None-Match 和資源的唯一標(biāo)識(shí)進(jìn)行對(duì)比,判斷是否使用緩存。
關(guān)于緩存的常見誤區(qū)
上面提到的知識(shí)估計(jì)就是平時(shí)大家最常背到的,不過(guò)大家有沒有認(rèn)真想過(guò)一個(gè)問(wèn)題?我們?nèi)〉降木彺鏀?shù)據(jù),一定緩存在瀏覽器里面嗎?
實(shí)際上是不然的:資源的緩存通常是有多級(jí)的,一些緩存專門用于單個(gè)用戶,一些緩存專用于多個(gè)用戶。有些是由服務(wù)器控制的,有些是由用戶控制的,有些則由中介層控制。


- 瀏覽器緩存:一般并專用于單個(gè)用戶,在瀏覽器客戶端中實(shí)現(xiàn)。它們通過(guò)避免多次獲取相同的響應(yīng)來(lái)提高性能。
- 本地代理:可能是用戶自己安裝的,也可能是由某個(gè)中介層管理的:比如公司的網(wǎng)絡(luò)層或者網(wǎng)絡(luò)提供商。本地代理通常會(huì)為多個(gè)用戶緩存單個(gè)響應(yīng),這就構(gòu)成了一種“公共”緩存。
- 源服務(wù)器緩存/CDN。由服務(wù)器控制,源服務(wù)器緩存的目標(biāo)是通過(guò)為多個(gè)用戶緩存相同的響應(yīng)來(lái)減少源服務(wù)器的負(fù)載。CDN 的目標(biāo)是相似的,但它分布在全球各個(gè)地區(qū),然后通過(guò)分配給最近的一組用戶來(lái)達(dá)到減少延遲的目的。
另外,我們也經(jīng)常會(huì)使用本地配置的代理,這些代理能夠通過(guò)配置信任證書來(lái)緩存 HTTPS 資源。
Spectre 漏洞
那么緩存怎么會(huì)對(duì)我們網(wǎng)站的安全性和用戶隱私造成威脅呢?我們來(lái)看一個(gè)非常有名的漏洞:Spectre。

攻擊者可以利用 Spectre 漏洞 來(lái)讀取操作系統(tǒng)進(jìn)程的內(nèi)存,這意味著可以訪問(wèn)到未經(jīng)過(guò)授權(quán)的跨域數(shù)據(jù)。
特別是在使用一些需要和計(jì)算機(jī)硬件進(jìn)行交互的 API 時(shí):
- SharedArrayBuffer (required for WebAssembly Threads)
- performance.measureMemory()
- JS Self-Profiling API
為此,瀏覽器一度禁用了 SharedArrayBuffer 等高風(fēng)險(xiǎn)的 API。
很多小伙伴對(duì)它具體的攻擊原理感興趣,通過(guò)幾個(gè) JavaScript API 怎么做到越權(quán)訪問(wèn)數(shù)據(jù)的?這個(gè)下次我會(huì)專門出個(gè)文章來(lái)講一下。
緩存是怎么影響 Spectre 的?
那么 Spectre 和緩存有啥關(guān)系呢?我們可以簡(jiǎn)單的這樣理解下:
我們正常打開一個(gè)收到跨域限制的頁(yè)面,肯定是獲取不到數(shù)據(jù)的。但是如果我們的 Cache-Control 設(shè)置為了 Public,這份數(shù)據(jù)可能會(huì)被緩存到一個(gè) Public Cache 上(比如我們本地代理的 Cache)。
雖然我們是沒有權(quán)限訪問(wèn)這個(gè)數(shù)據(jù)的,但是數(shù)據(jù)卻被存到緩存數(shù)據(jù)庫(kù)里了。一旦數(shù)據(jù)已經(jīng)被存下來(lái)了,攻擊者就可以利用 Spectre 漏洞獲取到這些緩存數(shù)據(jù)了。
那么為啥利用 Spectre 可以越權(quán)訪問(wèn)到這些緩存數(shù)據(jù)呢?我們來(lái)舉個(gè)簡(jiǎn)單的小例子:
比如,我們有個(gè)網(wǎng)站的登錄密碼是 conardli,一個(gè)攻擊者想要爆破我們的密碼,假設(shè)我們的密碼一定由小寫字母組成,那攻擊者也至少需要 26 的 8 次方次來(lái)猜出我們的密碼。這是一個(gè)非常大的數(shù)字,幾乎不可能爆破成功。
假設(shè),我們的密碼存在了一塊攻擊者無(wú)權(quán)限訪問(wèn)到的內(nèi)存里,然后攻擊者自己又單獨(dú)使用一塊內(nèi)存存儲(chǔ)了所有的26個(gè)英文字母,并把這段內(nèi)存設(shè)置為不可緩存。


那么這個(gè)時(shí)候,攻擊者越界訪問(wèn)了我們密碼的存儲(chǔ)區(qū)域,訪問(wèn)到了 c 這個(gè)字母,但是由于權(quán)限問(wèn)題,他肯定是訪問(wèn)不到的,會(huì)被計(jì)算機(jī)拒絕。

但是雖然訪問(wèn)不到,c 這個(gè)字母會(huì)被緩存起來(lái)。

這時(shí),攻擊者再回去遍歷他那 26 個(gè)字母的內(nèi)存,會(huì)發(fā)現(xiàn),c 的訪問(wèn)速度變快了 ...

所以,你的密碼第一個(gè)數(shù)字是 c ...
這里就簡(jiǎn)單講一下,下篇文章我會(huì)專門來(lái)講一下 Spectre 漏洞,還是非常巧妙的 ... 感興趣的小伙伴可以再留言區(qū)告訴我。
網(wǎng)站的建議配置
因?yàn)樯厦娴膯?wèn)題,我們建議對(duì)所有比較重要的網(wǎng)站數(shù)據(jù)做下面的兩個(gè)配置:
禁用 Public Cache
設(shè)置 Cache-Control: private,這可以禁用掉所有 Public Cache(比如代理),這就減少了攻擊者跨界訪問(wèn)到公共內(nèi)存的可能性。
這里注意,private 這個(gè)值并不是一個(gè)獨(dú)立的值,比如他是可以和 max-age 進(jìn)行共存的,性能和 public 差不了多少,我們打開 Google 的網(wǎng)站來(lái)看一下:

設(shè)置適當(dāng)?shù)亩?jí)緩存 key
默認(rèn)情況下,我們?yōu)g覽器的緩存使用 URL 和 請(qǐng)求方法來(lái)做緩存 key 的。
這意味著,如果一個(gè)網(wǎng)站需要登錄,不同用戶的請(qǐng)求由于它們的請(qǐng)求URL和方法相同,數(shù)據(jù)會(huì)被緩存到一塊內(nèi)存里。
這顯然是有點(diǎn)問(wèn)題,我們可以通過(guò)設(shè)置 Vary: Cookie 來(lái)避免這個(gè)問(wèn)題。
當(dāng)用戶身份信息發(fā)生變化的時(shí)候,緩存的內(nèi)存也會(huì)發(fā)生變化。
當(dāng)然,如果你的資源是一個(gè)大家都可以訪問(wèn)的公共 CDN 資源,那你的緩存當(dāng)然是隨便設(shè)置了,如果你的資源數(shù)據(jù)是比較敏感的,建議走上面這兩個(gè)設(shè)置。




























