Cookie 是儲存在瀏覽器中的小型資料字串。它們是 HTTP 協定的組成部分,由 RFC 6265 規範定義。
Cookie 通常由網路伺服器使用回應 Set-Cookie
HTTP 標頭來設定。然後,瀏覽器會自動將它們新增到使用 Cookie
HTTP 標頭對相同網域發出的 (幾乎) 每個要求。
最廣泛的用途之一就是驗證
- 登入時,伺服器會在回應中使用
Set-Cookie
HTTP 標頭,以設定一個具有唯一「階段識別碼」的 Cookie。 - 下次將要求傳送至相同網域時,瀏覽器會透過
Cookie
HTTP 標頭,將 Cookie 傳送至網路。 - 因此,伺服器知道誰發出要求。
我們也可以使用 document.cookie
屬性,從瀏覽器存取 Cookie。
Cookie 及其屬性有很多棘手的問題。在本章中,我們將詳細說明這些問題。
從 document.cookie 讀取
您的瀏覽器是否從此網站儲存任何 Cookie?讓我們看看
// At javascript.info, we use Google Analytics for statistics,
// so there should be some cookies
alert( document.cookie ); // cookie1=value1; cookie2=value2;...
document.cookie
的值包含 name=value
對,以 ;
分隔。每個都是一個獨立的 Cookie。
若要尋找特定 Cookie,我們可以透過 ;
分割 document.cookie
,然後找到正確的名稱。我們可以使用正規表示法或陣列函數來執行此操作。
我們把它留作讀者的練習。此外,在本章的最後,您會找到用於處理 Cookie 的輔助函數。
寫入 document.cookie
我們可以寫入 document.cookie
。但它不是資料屬性,而是一個 存取器(getter/setter)。對它的指派會被特別處理。
對 document.cookie
的寫入操作只會更新其中提到的 Cookie,而不會觸及其他 Cookie。
例如,此呼叫會設定一個名稱為 user
、值為 John
的 Cookie
document.cookie = "user=John"; // update only cookie named 'user'
alert(document.cookie); // show all cookies
如果您執行它,您可能會看到多個 Cookie。這是因為 document.cookie=
操作不會覆寫所有 Cookie。它只會設定提到的 Cookie user
。
技術上來說,名稱和值可以包含任何字元。為了保持有效的格式,它們應該使用內建的 encodeURIComponent
函數進行跳脫。
// special characters (spaces) need encoding
let name = "my name";
let value = "John Smith"
// encodes the cookie as my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
alert(document.cookie); // ...; my%20name=John%20Smith
有一些限制
- 您一次只能使用
document.cookie
設定/更新一個 Cookie。 name=value
對在encodeURIComponent
之後,不應超過 4KB。因此,我們無法在 Cookie 中儲存任何龐大的內容。- 每個網域的 Cookie 總數限制在 20+ 左右,確切的限制取決於瀏覽器。
Cookie 有幾個屬性,其中許多屬性很重要,應該設定。
屬性會在 key=value
之後列出,以 ;
分隔,如下所示
document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
網域
網域=site.com
網域定義 Cookie 可存取的位置。不過,實際上還是有限制。我們無法設定任何網域。
沒有辦法讓 Cookie 從另一個二級網域存取,因此 other.com
永遠不會收到在 site.com
設定的 Cookie。
這是一個安全限制,允許我們將敏感資料儲存在 Cookie 中,而這些資料應該只在一個網站上可用。
預設情況下,Cookie 只能在設定它的網域存取。
請注意,預設情況下,Cookie 不會與子網域共用,例如 forum.site.com
。
// if we set a cookie at site.com website...
document.cookie = "user=John"
// ...we won't see it at forum.site.com
alert(document.cookie); // no user
…但這可以更改。如果我們希望允許 forum.site.com
等子網域取得在 site.com
設定的 Cookie,這是可能的。
要做到這一點,在 site.com
設定 cookie 時,我們應該明確將 domain
屬性設定為根網域:domain=site.com
。然後,所有子網域都會看到此類 cookie。
例如
// at site.com
// make the cookie accessible on any subdomain *.site.com:
document.cookie = "user=John; domain=site.com"
// later
// at forum.site.com
alert(document.cookie); // has cookie user=John
過去,domain=.site.com
(site.com
前面有一個點)的運作方式相同,允許子網域存取 cookie。網域名稱中的開頭點現在已忽略,但某些瀏覽器可能會拒絕設定包含此類點的 cookie。
總之,domain
屬性允許在子網域中存取 cookie。
路徑
path=/mypath
URL 路徑字首必須是絕對路徑。它讓 cookie 可供該路徑下的頁面存取。預設情況下,它是目前的目錄。
如果 cookie 設定為 path=/admin
,則它會顯示在頁面 /admin
和 /admin/something
上,但不會顯示在 /home
、/home/admin
或 /
上。
通常,我們應該將 path
設定為根目錄:path=/
,以讓所有網站頁面都可以存取 cookie。如果未設定此屬性,則會使用 此方法 計算預設值。
過期時間、最大年齡
預設情況下,如果 cookie 沒有這些屬性之一,則當瀏覽器/分頁關閉時,它就會消失。此類 cookie 稱為「階段性 cookie」
若要讓 cookie 在瀏覽器關閉後仍然存在,我們可以設定 expires
或 max-age
屬性。如果同時設定,max-Age
優先。
expires=Tue, 19 Jan 2038 03:14:07 GMT
cookie 過期日期定義了瀏覽器將自動刪除它的時間(根據瀏覽器的時區)。
日期必須完全採用此格式,時區為 GMT。我們可以使用 date.toUTCString
來取得它。例如,我們可以設定 cookie 在 1 天後過期
// +1 day from now
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;
如果我們將 expires
設定為過去的日期,則 cookie 會被刪除。
max-age=3600
它是 expires
的替代方案,並以秒為單位指定 cookie 從目前時刻開始的過期時間。
如果設定為零或負值,則 cookie 會被刪除
// cookie will die in +1 hour from now
document.cookie = "user=John; max-age=3600";
// delete cookie (let it expire right now)
document.cookie = "user=John; max-age=0";
安全
安全
Cookie 應該只透過 HTTPS 傳輸。
預設情況下,如果我們在 http://site.com
設定 Cookie,它也會出現在 https://site.com
,反之亦然。
也就是說,Cookie 是基於網域的,它們不會區分通訊協定。
透過這個屬性,如果 Cookie 是由 https://site.com
設定的,那麼當透過 HTTP 存取同一個網站時,它不會出現,例如 http://site.com
。因此,如果 Cookie 具有不應透過未加密 HTTP 傳送的敏感內容,secure
旗標就是正確的選擇。
// assuming we're on https:// now
// set the cookie to be secure (only accessible over HTTPS)
document.cookie = "user=John; secure";
samesite
這是另一個安全性屬性 samesite
。它旨在防範所謂的 XSRF(跨網站請求偽造)攻擊。
為了了解它的運作方式和何時有用,讓我們來看看 XSRF 攻擊。
XSRF 攻擊
想像一下,你已登入網站 bank.com
。也就是說:你擁有該網站的驗證 Cookie。你的瀏覽器會在每次請求時將它傳送給 bank.com
,以便它識別你並執行所有敏感的金融作業。
現在,當你在另一個視窗瀏覽網路時,你意外地來到另一個網站 evil.com
。該網站有 JavaScript 程式碼,會將表單 <form action="https://bank.com/pay">
提交給 bank.com
,其中包含會將交易啟動到駭客帳戶的欄位。
瀏覽器會在每次你造訪網站 bank.com
時傳送 Cookie,即使表單是從 evil.com
提交的也是如此。因此,銀行會識別你並執行付款。
這就是所謂的「跨網站請求偽造」(簡稱 XSRF)攻擊。
當然,真正的銀行會受到保護。由 bank.com
產生的所有表單都有特殊欄位,也就是所謂的「XSRF 保護權杖」,惡意網頁無法產生或從遠端網頁中擷取它。它可以在那裡提交表單,但無法取回資料。網站 bank.com
會在它收到的每個表單中檢查是否有這樣的權杖。
不過,這樣的保護需要時間來實作。我們需要確保每個表單都有必要的權杖欄位,而且我們也必須檢查所有請求。
使用 Cookie samesite 屬性
Cookie samesite
屬性提供了另一種防範此類攻擊的方式,理論上不需要「xsrf 保護權杖」。
它有兩個可能的值
samesite=strict
如果使用者來自同一個網站以外,samesite=strict
的 Cookie 絕不會被傳送。
換句話說,無論使用者是從他們的電子郵件追蹤連結、從 evil.com
提交表單,或執行任何來自其他網域的作業,Cookie 都不會被傳送。
如果驗證 Cookie 具有 samesite=strict
屬性,那麼 XSRF 攻擊將沒有機會成功,因為來自 evil.com
的提交沒有 Cookie。因此,bank.com
將不會識別使用者,也不會繼續進行付款。
保護相當可靠。只有來自 bank.com
的操作會傳送 samesite=strict
cookie,例如從 bank.com
上的另一個頁面提交表單。
不過,有一個小不便。
當使用者從他們的筆記等地方追蹤合法連結到 bank.com
時,他們會驚訝地發現 bank.com
沒有辨識出他們。的確,在這種情況下,samesite=strict
cookie 沒有傳送。
我們可以使用兩個 cookie 來解決這個問題:一個用於「一般辨識」,只會說:「你好,John」,另一個用於資料變更操作,並設定 samesite=strict
。然後,從網站外部進來的人會看到歡迎訊息,但付款必須從銀行的網站發起,才能傳送第二個 cookie。
samesite=lax
(與沒有值的samesite
相同)
一種更輕鬆的方法,它也能防範 XSRF,而且不會破壞使用者體驗。
Lax 模式與 strict
一樣,禁止瀏覽器在從網站外部進入時傳送 cookie,但它增加了一個例外。
如果符合以下兩個條件,就會傳送 samesite=lax
cookie
-
HTTP 方法是「安全的」(例如 GET,但不是 POST)。
安全 HTTP 方法的完整清單載於 RFC7231 規範 中。這些方法應該用於讀取資料,但不用於寫入資料。它們不得執行任何資料變更操作。追蹤連結永遠都是 GET,也就是安全的方法。
-
操作執行頂層導覽(變更瀏覽器網址列中的網址)。
這通常是正確的,但如果導覽是在
<iframe>
中執行,那麼它就不是頂層的。此外,用於網路要求的 JavaScript 方法不會執行任何導覽。
因此,samesite=lax
的作用是允許最常見的「前往網址」操作擁有 cookie。例如,從筆記中開啟符合這些條件的網站連結。
但任何更複雜的操作,例如來自另一個網站的網路要求或提交表單,都會失去 cookie。
如果這樣對你來說沒問題,那麼新增 samesite=lax
可能不會破壞使用者體驗,而且還會增加保護。
總的來說,samesite
是個很棒的屬性。
有一個缺點
samesite
會被非常舊的瀏覽器(大約 2017 年左右)忽略(不支援)。
因此,如果我們只依賴 samesite
來提供保護,那麼舊的瀏覽器就會容易受到攻擊。
但我們可以使用 samesite
與其他防護措施(例如 xsrf 令牌)一起使用,以增加一層防護,然後在未來,當舊瀏覽器淘汰後,我們可能會放棄 xsrf 令牌。
httpOnly
此屬性與 JavaScript 無關,但我們必須完整地提到它。
網路伺服器使用 Set-Cookie
標頭設定 cookie。此外,它可能會設定 httpOnly
屬性。
此屬性禁止任何 JavaScript 存取 cookie。我們無法使用 document.cookie
查看或操作此類 cookie。
這用作預防措施,以防範某些攻擊,例如駭客將自己的 JavaScript 程式碼插入頁面並等待使用者造訪該頁面。這根本不應該發生,駭客不應該能夠將他們的程式碼插入我們的網站,但可能會有一些錯誤讓他們能夠這麼做。
通常,如果發生這種情況,而使用者造訪包含駭客 JavaScript 程式碼的網頁,則該程式碼會執行並取得對 document.cookie
的存取權,其中包含驗證資訊的使用者 cookie。這很糟糕。
但如果 cookie 是 httpOnly
,則 document.cookie
看不到它,因此受到保護。
附錄:Cookie 函式
以下是一組小型函式,可搭配 cookie 使用,比手動修改 document.cookie
更方便。
有許多 cookie 函式庫可供使用,因此這些函式庫僅供示範用途。但功能齊全。
getCookie(name)
存取 cookie 的最簡便方法是使用 正規表示法。
函式 getCookie(name)
傳回具有指定 name
的 cookie
// returns the cookie with the given name,
// or undefined if not found
function getCookie(name) {
let matches = document.cookie.match(new RegExp(
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
));
return matches ? decodeURIComponent(matches[1]) : undefined;
}
在此,會動態產生 new RegExp
,以符合 ; name=<value>
。
請注意,cookie 值已編碼,因此 getCookie
使用內建的 decodeURIComponent
函式對其進行解碼。
setCookie(name, value, attributes)
將 cookie 的 name
設定為指定的 value
,預設為 path=/
(可修改以新增其他預設值)
function setCookie(name, value, attributes = {}) {
attributes = {
path: '/',
// add other defaults here if necessary
...attributes
};
if (attributes.expires instanceof Date) {
attributes.expires = attributes.expires.toUTCString();
}
let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
for (let attributeKey in attributes) {
updatedCookie += "; " + attributeKey;
let attributeValue = attributes[attributeKey];
if (attributeValue !== true) {
updatedCookie += "=" + attributeValue;
}
}
document.cookie = updatedCookie;
}
// Example of use:
setCookie('user', 'John', {secure: true, 'max-age': 3600});
deleteCookie(name)
若要刪除 cookie,我們可以使用負到期日呼叫它
function deleteCookie(name) {
setCookie(name, "", {
'max-age': -1
})
}
請注意:當我們更新或刪除 cookie 時,我們應該使用與設定 cookie 時完全相同的路徑和網域屬性。
合在一起: cookie.js。
附錄:第三方 cookie
如果 cookie 是由使用者正在造訪的頁面以外的網域放置,則稱為「第三方」cookie。
例如
-
site.com
上的網頁從另一個網站載入橫幅廣告:<img src="https://ads.com/banner.png">
。 -
除了橫幅廣告之外,
ads.com
上的遠端伺服器可能會設定Set-Cookie
標頭,其中包含像id=1234
這樣的 Cookie。此類 Cookie 來自ads.com
網域,而且只會在ads.com
上可見 -
下次存取
ads.com
時,遠端伺服器會取得id
Cookie 並辨識使用者 -
更重要的是,當使用者從
site.com
移至另一個網站other.com
(該網站也有一個橫幅廣告)時,ads.com
會取得 Cookie,因為它屬於ads.com
,因此它會辨識訪客並追蹤訪客在網站之間的移動
由於其特性,第三方 Cookie 傳統上用於追蹤和廣告服務。它們與原始網域相關聯,因此如果所有網站都存取 ads.com
,ads.com
就可以在不同網站之間追蹤同一個使用者。
自然而然地,有些人不喜歡被追蹤,因此瀏覽器允許他們停用此類 Cookie。
此外,一些現代瀏覽器對此類 Cookie 採用特殊政策
- Safari 完全不允許第三方 Cookie。
- Firefox 附帶一個第三方網域「黑名單」,它會在其中封鎖第三方 Cookie。
如果我們從第三方網域載入指令碼,例如 <script src="https://google-analytics.com/analytics.js">
,而且該指令碼使用 document.cookie
設定 Cookie,則此類 Cookie 不是第三方 Cookie。
如果指令碼設定 Cookie,則無論指令碼從何處而來,Cookie 都屬於目前網頁的網域。
附錄:GDPR
這個主題與 JavaScript 完全無關,只是在設定 Cookie 時需要記住的事情。
歐洲有一項稱為 GDPR 的法規,它強制執行一組規則,要求網站尊重使用者的隱私。其中一項規則是要求使用者明確允許追蹤 Cookie。
請注意,這只與追蹤/識別/授權 Cookie 有關。
因此,如果我們設定一個 Cookie 只是為了儲存一些資訊,但既不追蹤也不識別使用者,我們就可以自由地這麼做。
但是,如果我們要設定一個包含驗證階段或追蹤 ID 的 Cookie,使用者必須允許這麼做。
網站通常有兩種符合 GDPR 的變體。您可能在網路上都看過它們
-
如果網站只想針對已驗證使用者設定追蹤 Cookie。
為此,註冊表單應包含一個核取方塊,例如「接受隱私權政策」(說明如何使用 Cookie),使用者必須勾選該方塊,然後網站才能自由設定驗證 Cookie。
-
如果網站想為所有人設定追蹤 Cookie。
為此,網站會為新訪客顯示一個模態「歡迎畫面」,並要求他們同意 Cookie。然後,網站可以設定 Cookie,並讓使用者看到內容。不過,這可能會讓新訪客感到困擾。沒有人喜歡看到「必須按一下」的模態歡迎畫面,而不是內容。但 GDPR 要求明確同意。
GDPR 不僅與 Cookie 有關,也與其他與隱私相關的問題有關,但這超出了我們的範圍。
摘要
document.cookie
提供對 Cookie 的存取權。
- 寫入操作只會修改其中提到的 Cookie。
- 名稱/值必須編碼。
- 一個 Cookie 的大小不得超過 4KB。一個網域允許的 Cookie 數量約為 20 個以上(因瀏覽器而異)。
Cookie 屬性
path=/
,預設為目前的目錄,使 Cookie 僅在該目錄下可見。domain=site.com
,預設情況下,Cookie 僅在目前的網域中可見。如果明確設定網域,Cookie 將在子網域中可見。expires
或max-age
設定 Cookie 的到期時間。如果沒有這些設定,Cookie 會在瀏覽器關閉時失效。secure
使 Cookie 僅限於 HTTPS。samesite
禁止瀏覽器傳送來自網站外部要求的 Cookie。這有助於防止 XSRF 攻擊。
此外
- 瀏覽器可能會禁止第三方 Cookie,例如 Safari 預設會這麼做。Chrome 也正在進行實作這項功能。
- 在為歐盟公民設定追蹤 Cookie 時,GDPR 要求取得許可。
留言
<code>
標籤,對於多行程式碼 - 將它們包裝在<pre>
標籤中,對於超過 10 行的程式碼 - 使用沙盒 (plnkr、jsbin、codepen…)