2024 年 2 月 13 日

Cookie、document.cookie

Cookie 是儲存在瀏覽器中的小型資料字串。它們是 HTTP 協定的組成部分,由 RFC 6265 規範定義。

Cookie 通常由網路伺服器使用回應 Set-Cookie HTTP 標頭來設定。然後,瀏覽器會自動將它們新增到使用 Cookie HTTP 標頭對相同網域發出的 (幾乎) 每個要求。

最廣泛的用途之一就是驗證

  1. 登入時,伺服器會在回應中使用 Set-Cookie HTTP 標頭,以設定一個具有唯一「階段識別碼」的 Cookie。
  2. 下次將要求傳送至相同網域時,瀏覽器會透過 Cookie HTTP 標頭,將 Cookie 傳送至網路。
  3. 因此,伺服器知道誰發出要求。

我們也可以使用 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.comsite.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 在瀏覽器關閉後仍然存在,我們可以設定 expiresmax-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

  1. HTTP 方法是「安全的」(例如 GET,但不是 POST)。

    安全 HTTP 方法的完整清單載於 RFC7231 規範 中。這些方法應該用於讀取資料,但不用於寫入資料。它們不得執行任何資料變更操作。追蹤連結永遠都是 GET,也就是安全的方法。

  2. 操作執行頂層導覽(變更瀏覽器網址列中的網址)。

    這通常是正確的,但如果導覽是在 <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。

例如

  1. site.com 上的網頁從另一個網站載入橫幅廣告:<img src="https://ads.com/banner.png">

  2. 除了橫幅廣告之外,ads.com 上的遠端伺服器可能會設定 Set-Cookie 標頭,其中包含像 id=1234 這樣的 Cookie。此類 Cookie 來自 ads.com 網域,而且只會在 ads.com 上可見

  3. 下次存取 ads.com 時,遠端伺服器會取得 id Cookie 並辨識使用者

  4. 更重要的是,當使用者從 site.com 移至另一個網站 other.com(該網站也有一個橫幅廣告)時,ads.com 會取得 Cookie,因為它屬於 ads.com,因此它會辨識訪客並追蹤訪客在網站之間的移動

由於其特性,第三方 Cookie 傳統上用於追蹤和廣告服務。它們與原始網域相關聯,因此如果所有網站都存取 ads.comads.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 的變體。您可能在網路上都看過它們

  1. 如果網站只想針對已驗證使用者設定追蹤 Cookie。

    為此,註冊表單應包含一個核取方塊,例如「接受隱私權政策」(說明如何使用 Cookie),使用者必須勾選該方塊,然後網站才能自由設定驗證 Cookie。

  2. 如果網站想為所有人設定追蹤 Cookie。

    為此,網站會為新訪客顯示一個模態「歡迎畫面」,並要求他們同意 Cookie。然後,網站可以設定 Cookie,並讓使用者看到內容。不過,這可能會讓新訪客感到困擾。沒有人喜歡看到「必須按一下」的模態歡迎畫面,而不是內容。但 GDPR 要求明確同意。

GDPR 不僅與 Cookie 有關,也與其他與隱私相關的問題有關,但這超出了我們的範圍。

摘要

document.cookie 提供對 Cookie 的存取權。

  • 寫入操作只會修改其中提到的 Cookie。
  • 名稱/值必須編碼。
  • 一個 Cookie 的大小不得超過 4KB。一個網域允許的 Cookie 數量約為 20 個以上(因瀏覽器而異)。

Cookie 屬性

  • path=/,預設為目前的目錄,使 Cookie 僅在該目錄下可見。
  • domain=site.com,預設情況下,Cookie 僅在目前的網域中可見。如果明確設定網域,Cookie 將在子網域中可見。
  • expiresmax-age 設定 Cookie 的到期時間。如果沒有這些設定,Cookie 會在瀏覽器關閉時失效。
  • secure 使 Cookie 僅限於 HTTPS。
  • samesite 禁止瀏覽器傳送來自網站外部要求的 Cookie。這有助於防止 XSRF 攻擊。

此外

  • 瀏覽器可能會禁止第三方 Cookie,例如 Safari 預設會這麼做。Chrome 也正在進行實作這項功能。
  • 在為歐盟公民設定追蹤 Cookie 時,GDPR 要求取得許可。
教學地圖

留言

留言前請先閱讀…
  • 如果你有建議要如何改進 - 請 提交 GitHub 問題 或提交 Pull Request,而不是留言。
  • 如果你無法理解文章中的某些內容 - 請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,對於多行程式碼 - 將它們包裝在 <pre> 標籤中,對於超過 10 行的程式碼 - 使用沙盒 (plnkrjsbincodepen…)