​Cookie 從入門到進階:一文徹底弄懂其原理以及應用

來自秦一授權的分享

Fortune cookie

Cookie,它的名字源自一種叫 Fortune cookie 的餅乾,這種餅乾裏面有一張寫着精闢句子的小紙條。

在瀏覽器中,Cookie 是服務器讓瀏覽器幫忙攜帶信息的手段,就像餅乾裏的紙條,瀏覽器會儲存它,並且在後續的 HTTP 請求中再次發送給服務器

主要用於以下三個方面:

因爲 HTTP 是無狀態的,所以爲了協助 Web 保持狀態,Cookie 誕生了。在 HTML5 的 localStorage、sessionStorage 出現之前,它作爲當時唯一的儲存手段也曾一度被用於客戶端儲存。

隨着瀏覽器儲存機制的完善,爲了減小不必要的性能開銷(因爲每次請求瀏覽器都會攜帶 Cookie 數據),一些客戶端需要而服務器不需要的數據的場景漸漸被其他儲存方式替代,例如記住用戶的主題信息,Cookie 的應用場景也漸漸迴歸初心。

目前 Cookie 主要用於會話狀態管理,以用戶登錄 - 退出登陸爲例,Cookie 的生命週期如下:

前端通過用戶登錄 API 向後端傳遞用戶信息,後端覈對與數據庫信息是否匹配。

匹配後在登錄 API 返回頭部 set-cookie 返回記錄用戶狀態的 cookie 值 userToken:

瀏覽器按照 set-cookie 的規則解析後存入瀏覽器

後續瀏覽器會自動將 userToken 加到滿足條件(域名、路徑)的 API 的 請求頭部 cookie 中

如果退出登陸,返回頭部的 set-cookie 會拜託瀏覽器幫忙刪除 userToken,瀏覽器的 cookie 儲存庫就會將 userToken 字段刪除,後續的 API 請求頭部 cookie 也不會發送它

服務端和瀏覽器有不同設置 Cookie 的方式。

速查表

ULu8in

詳細說明

服務端以 Node.js 爲例,不同語言有不同的用法,但 Cookie 設置邏輯是一樣的

const http = require("http");
http
  .createServer((req, res) => {
    if (req.url === "/read") {
      // 讀取 Cookie
      res.end(`Read Cookie: ${req.headers.cookie || ""}`);
    } else if (req.url === "/write") {
      // 設置 Cookie
      res.setHeader("Set-Cookie", [
        `name=scar;`,
        //set-cookie 屬性大小寫不敏感,你可以寫成 path=/ 或者 Path=/
        `language=javascript;Path=/; HttpOnly;Expires=${new Date(
          Date.now() + 1000
        ).toUTCString()};`,
      ]);
      res.end("Write Success");
    } else if (req.url === "/delete") {
      // 刪除 cookie
      res.setHeader("Set-Cookie", [
        // 設置過期時間爲過去的時間
        `name=;expires=${new Date(1).toUTCString()}`,
        // 有效期 max-age 設置成 0 或 -1 這種無效秒,讓 cookie 當場去世
        // 有些瀏覽器不支持 max-age 屬性,所以用此方法需要考慮兼容性
        "language=javascript; max-age=0",
      ]);
      res.end("Delete Success");
    } else {
      res.end("Not Found");
    }
  })
  .listen(3000);

客戶端:document.cookie

客戶端通過瀏覽器方法 document.cookie 讀寫當前界面的 Cookie。

// 編輯 ookie
document.cookie = ";
document.cookie = "language=javascript";
// 讀取 Cookie
console.log(document.cookie);
//name=scar; language=javascript

// 刪除 Cookie
document.cookie = ";

Cookie Store API 目前正在試驗階段,Firefox、Safari 瀏覽器 還不支持,所以不建議在生產環境使用,相信在將來我們會用上它更方便地操作 Cookie。

// 讀取 Cookie
await cookieStore.get("enName");
await cookieStore.getAll();

// 設置 Cookie
const day = 24 * 60 * 60 * 1000;
cookieStore
  .set({
    name: "enName",
    value: "scar",
    expires: Date.now() + day,
    domain: "scar.siteÏ",
  })
  .then(
    function () {
      console.log("It worked!");
    },
    function (reason) {
      console.error("It failed: ", reason);
    }
  );

// 刪除 Cookie
await cookieStore.delete("session_id");

// 監聽 Cookie 變化
cookieStore.addEventListener("change", (event) => {
  for (const cookie of event.changed) {
    if (cookie.name === "name") sessionCookieChanged(cookie.value);
  }
  for (const cookie of event.deleted) {
    if (cookie.name === "enName") sessionCookieChanged(null);
  }
});

除了更方便的用法,他還有以下特性:

異步操作

它可以異步訪問 Cookie,不阻塞主進程,document.cookie 是同步操作。

錯誤拋出機制

Cookie Store API 有一個明確的機制來報告 Cookie 存儲錯誤,而 document.cookie 如果設置失敗也不會提醒,所以需要輪詢查 Cookie 的方法來確保設置成功。

service workers 支持

因爲 document.cookie 的同步設計,所以 service workers 不支持。Cookie Store API 的異步特性更適合,所以 service workers 支持通過它訪問 Cookie。

從語法可以看出,Set-Cookie 由前綴、鍵值對、屬性三部分組成。

Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>

// 同時指定多個屬性 Domain、Secure、HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

// cookie 前綴,值可能性爲 __Secure-、__Host-
Set-Cookie: <cookie-prefix><cookie-name>=<cookie-value>

Cookie 前綴是一種在 Cookie 名稱中攜帶信息的方式,它必須和某些屬性同時出現,否則 Cookie 無法設置成功。

rsS4nt

例子:

// 支持 Cookie 前綴的收到下面設置的時候會拒絕,因爲沒有同時設置 Secure 屬性
document.cookie = "__Secure-invalid-without-secure=1";
// 這樣設置才能成功!
document.cookie = "__Secure-valid-with-secure=1; Secure";
// 當響應來自於一個安全域(HTTPS)的時候,二者都可以被客戶端接受
Set-Cookie: __Secure-ID=123; Secure; Domain=example.com
Set-Cookie: __Host-ID=123; Secure; Path=/

// 缺少 Secure 屬性,會被拒絕
Set-Cookie: __Secure-id=1

// 缺少 Path=/ ,會被拒絕
Set-Cookie: __Host-id=1; Secure

說實話這個前綴我之前從沒見過,孤陋寡聞了。

除了 IE 不支持,其他各大瀏覽器基本支持。不同瀏覽器限制可能不同,例如:Set-Cookie: __Host-id=1; Secure設置 __Host-前綴的時候,即使缺少 Path=/,FireFox 可以設置成功,而 Chrome 會拒絕。

但爲什麼有 Secure 屬性還要加個 __Secure- 前綴呢?

因爲 Secure 屬性設置後是可以被人惡意移除的,而 Cookie 名稱被人移除前綴,服務器不會認它,所以更加安全。

<cookie-name>=<cookie-value>

真正攜帶信息的部分,例如:

id=38afes7a8

<cookie-name>  可以是除了控制字符 (CTLs)、空格 (spaces) 或製表符 (tab) 之外的任何 US-ASCII 字符。同時不能包含以下分隔字符:( ) < > @ , ; : \ " /  [ ] ? = { }。

<cookie-value>  非必填,如果有值,那麼需要包含在雙引號裏面。支持除了控制字符(CTLs)、空格(whitespace)、雙引號(double quotes)、逗號(comma)、分號(semicolon)以及反斜線(backslash)之外的任意 US-ASCII 字符。

Cookie 屬性可以理解爲 Cookie 的配置項,告訴瀏覽器 Cookie 的一些額外信息,例如什麼時候失效

速查表

q69szB

詳細說明

Domain

Domain 指定了哪些主機地址可以接收 Cookie。來看看如下設置:

Set-Cookie: __Secure-ID=123; Secure; Domain=example.com

表示只要請求的目標地址匹配 Domain 規則,那 Cookie 就會被髮送過去,所即使scar.siteexample.com 發起的請求,Cookie 會被髮送過去。所以不要再誤會 Domain是發起請求的域名啦,其實是接受請求的域名

如果不設置,默認爲 origin,不包含子域名,如果指定了Domain,則一般包含子域名,例如,如果設置 Domain=mozilla.org,則 Cookie 也包含在子域名中(如developer.mozilla.org)。

當前大多數瀏覽器遵循 RFC 6265,設置 Domain 時 不需要加前導點。瀏覽器不遵循該規範,則需要加前導點,例如:Domain=.mozilla.org

Path

Path 標識指定了主機下的哪些路徑可以接受 Cookie(該 URL 路徑必須存在於請求 URL 中)。以字符 %x2F ("/") 作爲路徑分隔符,子路徑也會被匹配。

例如,設置 Path=/docs,則以下地址都會匹配:

Expires

設置過期時間,可以傳一個符合 HTTP-Date 格式的值,例如:expires=Mon, 14 Mar 2022 15:39:34 GMT;

如果不設置 Expires,則默認爲會話關閉時間;

會話是瀏覽器的一個概念,一般是一個瀏覽器 Tab 窗口。

如果瀏覽器提供了會話恢復功能,恢復回話之時,Cookie 也會一起恢復,就好像會話從來沒有關閉一樣。

Max-Age

在 Cookie 失效之前需要經過的秒數。秒數爲 0 或 -1 將會使 cookie 直接過期。一些老的瀏覽器(IE 6、IE 7 和 IE 8)不支持這個屬性。

如果 Expires 和Max-Age 同時存在時,Max-Age優先級更高。

HttpOnly

設置了 HttpOnly 屬性的 cookie 不能使用 JavaScript 經由  Document.cookie 屬性、XMLHttpRequest 和  Request APIs、Cookie Store APIs 進行訪問。

Secure

標記爲 Secure 的 Cookie 只應通過被 HTTPS 協議加密過的請求發送給服務端,因此可以預防 man-in-the-middle 攻擊。

但即便設置了 Secure 標記,敏感信息也不應該通過 Cookie 傳輸,因爲 Cookie 固有的不安全性,Secure 標記也無法提供確實的安全保障,例如:可以訪問客戶端硬盤的人可以讀取它。

SameSite

服務器要求某個 Cookie 在跨站請求時不會被髮送,從而可以阻止跨站請求僞造攻擊。

SameSite 可以有下面三種值:

SameSite 和 Domain 的區別

上面提到過,Domain 可以指定 Cookie 生效的域名,那它和 Domain的區別是什麼呢?

Domain 屬性限制了接收 Cookie 的域名,而 SameSite屬性限制了發送 Cookie 的域名

舉個例子:

Set-Cookie: Foo=bar; Path=/; Secure; Domain=scar.site;

無論是從 scar.site 還是 foo.example.com 發起的請求,只要被髮送到了 scar.site 或者它的子域名,那 Cookie 就能發過去。

Set-Cookie: name=scar; Path=/; Secure; Domain=scar.site;SameSite=strict;

如果設置了 SameSite=strict, 那麼這個請求只能從 scar.site 發起 Cookie 才能帶過去。

SameParty

目前還在實驗階段

配合  First-Party Sets 實現跨域共享屬性,詳細可以訪問:詳解 Cookie 新增的 SameParty 屬性。

Priority

目前只有 Chrome 實現了這個提案

因爲 Cookie 有數量限制,所以在 Cookie 超過一定數量時,瀏覽器會清除最早過期的 Cookie。

如果設置了 Priority,Chrome 會先將優先級低的清除,並且每種優先級 Cookie 至少保留一個。可以有下面三種值:

Q&A

一些關於 Cookie 的疑問和新特性,以 Q&A 形式記錄。比較雜、比較散,可以說沒什麼知識點全是感情,屬於那種你知道了可能沒什麼用但是就是想把它弄懂。

大小限制

大多數瀏覽器支持最大爲 4KB 的 Cookie,4KB 是針對 Cookie 單條記錄的 Value 值。

數量限制

Cookie 有數量限制,而且只允許每個站點存儲一定數量的 Cookie,當超過時,最早過期的 Cookie 便被刪除。

不同瀏覽器支持的數量可能不同, 基於 Webkit 內核的是 180 個,基於 gecko 內核的是 150 個,感興趣可以訪問江濤學編程 - 編寫的 Cookie 實驗 試試自己的瀏覽器 Cookie 數量限制

實際上影響 Cookie 被刪除的要素不止是 ExpiresMax-Age,還有 PrioritySecure,對移除策略感興趣的可以看:Cookie 知識二則

CSRF 攻擊

CSRF:跨站請求攻擊,簡單地說,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問一個自己曾經認證過的網站並運行一些操作(如發郵件,發消息,甚至財產操作如轉賬和購買商品)。

舉個例子,一家銀行用以運行轉賬操作的 URL 如下:

http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName

那麼,一個惡意攻擊者可以在另一個網站上放置如下代碼:

<img src="<http://www.examplebank.com/withdraw?account=scar&amount=1000&for=Badman>">

如果有賬戶名爲 Alice 的用戶訪問了惡意站點,而她之前剛訪問過銀行不久,登錄信息尚未過期,導致發起請求後後端以爲是用戶正常操作,於是進行扣款操作,那麼她就會損失 1000 資金。通過設置 sameSite 可以防止跨域發送 Cookie,抵禦 CSRF。

XSS 攻擊

跨站腳本(Cross-site scripting)是一種網站應用程序的安全漏洞攻擊,簡稱爲 CSS, 但這會與層疊樣式表(Cascading Style Sheets)CSS 的縮寫混淆。因此,跨站腳本攻擊縮寫爲 XSS。

XSS 攻擊通常指的是通過利用網頁開發時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網頁,使用戶加載並執行攻擊者惡意製造的網頁程序。攻擊成功後,攻擊者可能得到 Cookie 從而實現攻擊。

引用自:如果不用第三方 Cookie,Google FLoC 會是更好的替代者嗎?- 少數派

FLoC 是一種新的廣告追蹤技術,全稱爲 Federated Learning of Cohorts,即「同類羣組聯合學習」。FLoC 的工作原理是監視你的瀏覽記錄,爲訪客的彙總行爲分配一個 ID,然後將具有類似瀏覽行爲的瀏覽器分組在一起。這些羣組的數據稱爲同類羣組,然後用於向人們展示針對性更強的廣告。

FLoC 在自身設計層面,是比 Cookie 隱私性更好的,但是首先它依然是一個廣告追蹤技術,其次纔是一個相對保護隱私的廣告追蹤技術。

FLoC 由 Google 主導,所以三方團體擔憂:當所有的瀏覽器開始默認屏蔽第三方 Cookie,廣告商轉向使用 FLoC 以後,Google 將在廣告追蹤市場一家獨大。

Cookie: a=2; a=1

首先來看看 Cookie 發送順序,RFC 6265 提案提到:

具體的瀏覽器表現我沒有去探究,但提案只是倡導,所以每個客戶端不一定會按照它實現 Cookie 的發送順序。

除了考慮發送順序,還要考慮不同的服務器框架可能有不同的接收邏輯,所以筆者推薦儘量避免出現同名 Cookie,減少端表現不統一帶來的不確定性。

F12 打開控制檯可以快速看到本域下的所有 Cookie

通過分析 Cookie 屬性來定位問題。

例如某個 Cookie 導致了業務問題,如果它設置了 HttpOnly,那麼代表客戶端無法操作 Cookie,可以快速的把問題定位到 API 層面。

總結

本來我在寫:Cookie、Session、Token ,寫着寫着發現 Cookie 的篇幅比較多,而那篇文章的重點不在於這些部分,所以摘了出來。如果對 Cookie、Session、Token 感興趣的可以持續關注一下我,近期會發哦~

作爲一名前端人員,平時用得少所以不熟悉,但瞭解後其實也沒有什麼難點,相信你們看完這篇就可以徹底瞭解了。如果文章中還有關於 Cookie 你想知道但是我沒寫的部分都可以評論,我會回覆。

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ljZ51xJZvG-8mx0CvU7vVA