使用 HTTP Cookie

使用 HTTP Cookie

Cookie 的用途

通常,伺服器會使用 HTTP Cookie 的內容來判斷不同的請求是否來自同一個瀏覽器/使用者,然後視情況發出個人化或通用的回應。以下描述一個基本的使用者登入系統:

使用者將登入憑證傳送給伺服器,例如透過表單提交。

如果憑證正確,伺服器會更新 UI 以表示使用者已登入,並回應一個包含 session ID 的 Cookie,該 Cookie 會在瀏覽器上記錄其登入狀態。

稍後,使用者移動到同一個網站上的不同頁面。瀏覽器會將包含 session ID 的 Cookie 連同對應的請求一起傳送,以表示它仍然認為使用者已登入。

伺服器會檢查 session ID,如果仍然有效,則會傳送個人化版本的新頁面給使用者。如果無效,則會刪除 session ID,並向使用者顯示通用版本的頁面(或可能顯示「存取被拒」的訊息並要求再次登入)。

Cookie 主要用於三個目的:

會話管理:使用者登入狀態、購物車內容、遊戲分數,或任何其他伺服器需要記住的使用者會話相關細節。

個人化:使用者偏好,例如顯示語言和 UI 主題。

追蹤:記錄和分析使用者行為。

資料儲存

在 Web 早期,當沒有其他選擇時,Cookie 被用於一般的用戶端資料儲存目的。現在建議使用現代的儲存 API,例如 Web Storage API(localStorage 和 sessionStorage)和 IndexedDB。

它們是為儲存而設計的,永遠不會將資料傳送到伺服器,並且沒有使用 Cookie 進行儲存的其他缺點:

瀏覽器通常限制每個網域的 Cookie 數量上限(因瀏覽器而異,通常在數百個左右),以及每個 Cookie 的大小上限(通常為 4KB)。儲存 API 可以儲存更大量的資料。

Cookie 會隨著每個請求一起傳送,因此可能會降低效能(例如在緩慢的行動數據連線上),特別是當你設定了很多 Cookie 時。

備註:要查看儲存的 Cookie(以及網頁正在使用的其他儲存),你可以在 Firefox 開發者工具中使用儲存空間檢測器,或在 Chrome 開發者工具中使用應用程式面板。

建立、移除與更新 Cookie

在收到 HTTP 請求後,伺服器可以在回應中傳送一個或多個 Set-Cookie 標頭,每個標頭都會設定一個獨立的 Cookie。設定 Cookie 的方式是指定一個鍵值對,如下所示:

httpSet-Cookie: =

以下 HTTP 回應指示接收的瀏覽器儲存一對 Cookie:

httpHTTP/2.0 200 OK

Content-Type: text/html

Set-Cookie: yummy_cookie=chocolate

Set-Cookie: tasty_cookie=strawberry

[頁面內容]

備註:了解如何在各種伺服器端語言/框架中使用 Set-Cookie 標頭:PHP、Node.js、Python、Ruby on Rails。

當發出新請求時,瀏覽器通常會將先前為目前網域儲存的 Cookie,在 Cookie HTTP 標頭中傳回給伺服器:

httpGET /sample_page.html HTTP/2.0

Host: www.example.org

Cookie: yummy_cookie=chocolate; tasty_cookie=strawberry

移除:定義 Cookie 的生命週期

你可以指定一個到期日期或時間段,在此之後 Cookie 應被刪除且不再傳送。根據建立 Cookie 時在 Set-Cookie 標頭中設定的屬性,它們可以是永久性或會話性 Cookie:

永久性 Cookie 會在 Expires 屬性指定的日期之後被刪除:

httpSet-Cookie: id=a3fWa; Expires=Thu, 31 Oct 2021 07:28:00 GMT;

或在 Max-Age 屬性指定的時間段之後:

httpSet-Cookie: id=a3fWa; Max-Age=2592000

備註:Expires 的可用時間比 Max-Age 長,但 Max-Age 較不易出錯,並且在兩者都設定時具有優先權。這背後的理由是,當你設定 Expires 的日期和時間時,它們是相對於設定 Cookie 的用戶端。如果伺服器的時間設定不同,可能會導致錯誤。

會話性 Cookie——沒有 Max-Age 或 Expires 屬性的 Cookie——會在目前會話結束時被刪除。瀏覽器定義了「目前會話」何時結束,有些瀏覽器在重新啟動時會使用會話還原。這可能導致會話性 Cookie 無限期地持續存在。

備註:如果你的網站對使用者進行身份驗證,它應該在使用者每次驗證時重新生成並重新傳送會話性 Cookie,即使是已經存在的 Cookie。這種方法有助於防止會話固定攻擊,在這種攻擊中,第三方可以重複使用使用者的會話。

有一些技術旨在在 Cookie 被刪除後重新建立它們。這些被稱為「殭屍」cookie。這些技術違反了使用者隱私和控制的原則,可能違反資料隱私法規,並可能使使用它們的網站面臨法律責任。

更新 Cookie 值

要透過 HTTP 更新 Cookie,伺服器可以傳送一個帶有現有 Cookie 名稱和新值的 Set-Cookie 標頭。例如:

httpSet-Cookie: id=new-value

你可能出於多種原因想要這樣做,例如,如果使用者更新了他們的偏好,而應用程式希望在用戶端資料中反映這些變更(你也可以使用用戶端儲存機制,如 Web Storage 來實現)。

透過 JavaScript 更新 Cookie

在瀏覽器中,你可以使用 Document.cookie 屬性或非同步的 Cookie Store API 透過 JavaScript 建立新的 Cookie。請注意,以下所有範例都使用 Document.cookie,因為它是最廣泛支援/最成熟的選項。

jsdocument.Cookie = "yummy_cookie=chocolate";

document.Cookie = "tasty_cookie=strawberry";

你也可以存取現有的 Cookie 並為其設定新值,前提是它們沒有設定 HttpOnly 屬性(即在建立它的 Set-Cookie 標頭中):

jsconsole.log(document.cookie);

// 輸出「yummy_cookie=chocolate; tasty_cookie=strawberry」

document.Cookie = "yummy_cookie=blueberry";

console.log(document.cookie);

// 輸出「tasty_cookie=strawberry; yummy_cookie=blueberry」

請注意,出於安全目的,你在初始化請求時不能直接透過傳送更新的 Cookie 標頭來更改 Cookie 值,例如,透過 fetch() 或 XMLHttpRequest。請注意,也有充分的理由不應允許 JavaScript 修改 Cookie——即在建立時設定 HttpOnly。有關更多詳細訊息,請參見安全性一節。

安全性

當你在 Cookie 中儲存訊息時,預設情況下,所有 Cookie 值對終端使用者都是可見的,並且可以被他們更改。你絕對不希望你的 Cookie 被濫用——例如被惡意行為者存取/修改,或被傳送到不應傳送的網域。潛在的後果可能從惱人的——應用程式無法運作或表現出奇怪的行為——到災難性的。例如,犯罪分子可以竊取一個 session ID,並用它來設定一個 Cookie,使其看起來像是以另一個人的身份登入,從而控制他們的銀行或電子商務帳戶。

你可以用多種方式保護你的 Cookie,本節將對此進行回顧。

阻擋對你 Cookie 的存取

你可以透過兩種方式確保 Cookie 安全傳送,且不會被非預期的各方或腳本存取:使用 Secure 屬性和 HttpOnly 屬性:

httpSet-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly

帶有 Secure 屬性的 Cookie 僅會透過 HTTPS 協定以加密請求傳送至伺服器。它永遠不會與不安全的 HTTP 一起傳送(localhost 除外),這意味著中間人攻擊者無法輕易存取它。不安全的網站(URL 中帶有 http:)無法設定帶有 Secure 屬性的 Cookie。但是,不要以為 Secure 可以防止對 Cookie 中敏感訊息的所有存取。例如,有權存取用戶端硬碟(或 JavaScript,如果未設定 HttpOnly 屬性)的人可以讀取和修改訊息。

帶有 HttpOnly 屬性的 Cookie 不能被 JavaScript 存取,例如使用 Document.cookie;它只能在到達伺服器時被存取。例如,持久化使用者會話的 Cookie 應該設定 HttpOnly 屬性——讓它們對 JavaScript 可用會非常不安全。這項預防措施有助於減輕跨網站指令碼攻擊(XSS)。

備註:根據應用程式的不同,你可能希望使用一個由伺服器查詢的不透明識別碼,而不是直接在 Cookie 中儲存敏感訊息,或者研究替代的身份驗證/機密性機制,例如 JSON Web Token。

定義 Cookie 的傳送位置

Domain 和 Path 屬性定義了 Cookie 的範圍:Cookie 被傳送到哪些 URL。

Domain 屬性指定哪個伺服器可以接收 Cookie。如果指定,Cookie 在指定的伺服器及其子網域上可用。例如,如果你從 mozilla.org 設定 Domain=mozilla.org,Cookie 在該網域和像 developer.mozilla.org 這樣的子網域上都可用。

httpSet-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly; Domain=mozilla.org

如果 Set-Cookie 標頭未指定 Domain 屬性,則 Cookie 在設定它的伺服器上可用,但不在其子網域上。因此,指定 Domain 比省略它限制更少。請注意,伺服器只能將 Domain 屬性設定為其自己的網域或父網域,而不能設定為子網域或其他網域。因此,例如,網域為 foo.example.com 的伺服器可以將屬性設定為 example.com 或 foo.example.com,但不能設定為 bar.foo.example.com 或 elsewhere.com(不過 Cookie 仍然會被傳送到像 bar.foo.example.com 這樣的子網域)。有關更多詳細訊息,請參見無效網域。

Path 屬性指示請求的 URL 中必須存在的 URL 路徑,以便傳送 Cookie 標頭。例如:

httpSet-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly; Path=/docs

%x2F(「/」)字元被視為目錄分隔符,子目錄也會匹配。例如,如果你設定 Path=/docs,這些請求路徑會匹配:

/docs

/docs/

/docs/Web/

/docs/Web/HTTP

但這些請求路徑不會:

/

/docsets

/fr/docs

備註:path 屬性讓你根據網站的不同部分來控制瀏覽器傳送哪些 Cookie。它不是作為安全措施,並且不能防止從不同路徑未經授權地讀取 Cookie。

使用 SameSite 控制第三方 Cookie

SameSite 屬性讓伺服器可以指定是否/何時隨跨站請求傳送 Cookie——即第三方 Cookie。跨站請求是指網站(可註冊的網域)和/或協定(http 或 https)與使用者目前正在訪問的網站不匹配的請求。這包括在其他網站上點擊連結以導航到你的網站時傳送的請求,以及由嵌入的第三方內容傳送的任何請求。

SameSite 有助於防止訊息洩漏,保護使用者隱私並提供一些針對跨站請求偽造攻擊的保護。它有三個可能的值:Strict、Lax 和 None:

Strict 會使瀏覽器僅在回應源自 Cookie 原始網站的請求時才傳送 Cookie。當你有與功能相關的 Cookie,而這些功能總是在初始導航之後,例如身份驗證或儲存購物車訊息時,應該使用此設定。

httpSet-Cookie: cart=110045_77895_53420; SameSite=Strict

備註:用於敏感訊息的 Cookie 也應該有較短的生命週期。

Lax 與此類似,只是瀏覽器在使用者導航到 Cookie 的原始網站時也會傳送 Cookie(即使使用者來自不同的網站)。這對於影響網站顯示的 Cookie 很有用——例如,你的網站上可能有合作夥伴的產品訊息以及一個聯盟連結。當使用者跟隨該連結到合作夥伴網站時,他們可能希望設定一個 Cookie,說明該聯盟連結被跟隨,如果購買了產品,則會顯示獎勵橫幅並提供折扣。

httpSet-Cookie: affiliate=e4rt45dw; SameSite=Lax

None 指定 Cookie 在原始請求和跨站請求中都會被傳送。如果你想將 Cookie 與從嵌入在其他網站中的第三方內容發出的請求一起傳送,例如廣告技術或分析提供商,這就很有用。請注意,如果設定了 SameSite=None,則還必須設定 Secure 屬性——SameSite=None 需要一個安全上下文。

httpSet-Cookie: widget_session=7yjgj57e4n3d; SameSite=None; Secure; HttpOnly

如果未設定 SameSite 屬性,則 Cookie 預設被視為 Lax。

Cookie 前綴

由於 Cookie 機制的設計,伺服器無法確認 Cookie 是否從安全來源設定,甚至無法知道 Cookie 最初是在哪裡設定的。

子網域上的應用程式可以設定帶有 Domain 屬性的 Cookie,這使得所有其他子網域都可以存取該 Cookie。這種機制可能在會話固定攻擊中被濫用。

然而,作為一種縱深防禦措施,你可以使用 Cookie 前綴來斷言關於 Cookie 的特定事實。有兩種前綴可用:

__Host-:如果 Cookie 名稱有這個前綴,它只有在同時標記了 Secure 屬性、從安全來源傳送、不包含 Domain 屬性,並且 Path 屬性設定為 / 的情況下,才會在 Set-Cookie 標頭中被接受。換句話說,這個 Cookie 是網域鎖定的。

__Secure-:如果 Cookie 名稱有這個前綴,它只有在標記了 Secure 屬性並從安全來源傳送的情況下,才會在 Set-Cookie 標頭中被接受。這比 __Host- 前綴弱。

瀏覽器會拒絕不符合其限制的帶有這些前綴的 Cookie。這確保了由子網域建立的帶有前綴的 Cookie 要麼被限制在一個子網域內,要麼被完全忽略。由於應用程式伺服器在確定使用者是否已驗證或 CSRF 權杖是否正確時只檢查特定的 Cookie 名稱,這實際上起到了防禦會話固定攻擊的作用。

備註:在伺服器上,網頁應用程式必須檢查包含前綴的完整 Cookie 名稱。使用者代理在請求的 Cookie 標頭中傳送 Cookie 之前,不會從 Cookie 中剝離前綴。

有關 Cookie 前綴和目前瀏覽器支援狀態的更多訊息,請參見 Set-Cookie 參考文章的前綴部分。

隱私與追蹤

前面我們談到如何使用 SameSite 屬性來控制何時傳送第三方 Cookie,以及這如何有助於保護使用者隱私。在建立網站時,隱私是一個非常重要的考量,如果做得好,可以與使用者建立信任。如果做得不好,則會完全侵蝕這種信任並導致各種其他問題。

第三方 Cookie 可以由透過