Javascript Cookie,document.cookie

2023-02-17 10:57 更新

Cookie 是直接存儲在瀏覽器中的一小串數(shù)據(jù)。它們是 HTTP 協(xié)議的一部分,由 RFC 6265 規(guī)范定義。

Cookie 通常是由 Web 服務器使用響應 Set-Cookie HTTP-header 設置的。然后瀏覽器使用 Cookie HTTP-header 將它們自動添加到(幾乎)每個對相同域的請求中。

最常見的用處之一就是身份驗證:

  1. 登錄后,服務器在響應中使用 ?Set-Cookie? HTTP-header 來設置具有唯一“會話標識符(session identifier)”的 cookie。
  2. 下次當請求被發(fā)送到同一個域時,瀏覽器會使用 ?Cookie? HTTP-header 通過網(wǎng)絡發(fā)送 cookie。
  3. 所以服務器知道是誰發(fā)起了請求。

我們還可以使用 document.cookie 屬性從瀏覽器訪問 cookie。

關于 cookie 及其選項,有很多棘手的事情。在本章中,我們將詳細介紹它們。

從 document.cookie 中讀取

你的瀏覽器是否存儲了本網(wǎng)站的任何 cookie?讓我們來看看:

// 在 javascript.info,我們使用谷歌分析來進行統(tǒng)計,
// 所以應該存在一些 cookie
alert( document.cookie ); // cookie1=value1; cookie2=value2;...

document.cookie 的值由 name=value 對組成,以 ; 分隔。每一個都是獨立的 cookie。

為了找到一個特定的 cookie,我們可以以 ; 作為分隔,將 document.cookie 分開,然后找到對應的名字。我們可以使用正則表達式或者數(shù)組函數(shù)來實現(xiàn)。

我們把這個留給讀者當作練習。此外,在本章的最后,你可以找到一些操作 cookie 的輔助函數(shù)。

寫入 document.cookie

我們可以寫入 document.cookie。但這不是一個數(shù)據(jù)屬性,它是一個 訪問器(getter/setter)。對其的賦值操作會被特殊處理。

對 document.cookie 的寫入操作只會更新其中提到的 cookie,而不會涉及其他 cookie。

例如,此調(diào)用設置了一個名稱為 user 且值為 John 的 cookie:

document.cookie = "user=John"; // 只會更新名稱為 user 的 cookie
alert(document.cookie); // 展示所有 cookie

如果你運行了上面這段代碼,你會看到多個 cookie。這是因為 document.cookie= 操作不是重寫整所有 cookie。它只設置代碼中提到的 cookie user。

從技術(shù)上講,cookie 的名稱和值可以是任何字符。為了保持有效的格式,它們應該使用內(nèi)建的 encodeURIComponent 函數(shù)對其進行轉(zhuǎn)義:

// 特殊字符(空格),需要編碼
let name = "my name";
let value = "John Smith"

// 將 cookie 編碼為 my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

alert(document.cookie); // ...; my%20name=John%20Smith

限制

存在一些限制:

  • ?encodeURIComponent? 編碼后的 ?name=value? 對,大小不能超過 4KB。因此,我們不能在一個 cookie 中保存大的東西。
  • 每個域的 cookie 總數(shù)不得超過 20+ 左右,具體限制取決于瀏覽器。

Cookie 有幾個選項,其中很多都很重要,應該設置它。

選項被列在 key=value 之后,以 ; 分隔,像這樣:

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"

path

  • ?path=/mypath?

url 路徑前綴必須是絕對路徑。它使得該路徑下的頁面可以訪問該 cookie。默認為當前路徑。

如果一個 cookie 帶有 path=/admin 設置,那么該 cookie 在 /admin 和 /admin/something 下都是可見的,但是在 /home 或 /adminpage 下不可見。

通常,我們應該將 path 設置為根目錄:path=/,以使 cookie 對此網(wǎng)站的所有頁面可見。

domain

  • ?domain=site.com?

domain 控制了可訪問 cookie 的域。但是在實際中,有一些限制。我們無法設置任何域。

無法從另一個二級域訪問 cookie,因此 other.com 永遠不會收到在 site.com 設置的 cookie。

這是一項安全限制,為了允許我們將敏感數(shù)據(jù)存儲在應該僅在一個站點上可用的 cookie 中。

默認情況下,cookie 只有在設置的域下才能被訪問到。

請注意,默認情況下,cookie 也不會共享給子域,例如 ?forum.site.com?。

// 如果我們在 site.com 網(wǎng)站上設置了 cookie……
document.cookie = "user=John"

// ……在 forum.site.com 域下我們無法訪問它
alert(document.cookie); // 沒有 user

……但這是可以設置的。如果我們想允許像 forum.site.com 這樣的子域在 site.com 上設置 cookie,也是可以實現(xiàn)的。

為此,當在 site.com 設置 cookie 時,我們應該明確地將 domain 選項設置為根域:domain=site.com。那么,所有子域都可以訪問到這樣的 cookie。

例如:

// 在 site.com
// 使 cookie 可以被在任何子域 *.site.com 訪問:
document.cookie = "user=John; domain=site.com"

// 之后

// 在 forum.site.com
alert(document.cookie); // 有 cookie user=John

出于歷史原因,domain=.site.comsite.com 前面有一個點符號)也以相同的方式工作,允許從子域訪問 cookie。這是一個舊的表示方式,如果我們需要支持非常舊的瀏覽器,那么應該使用它。

總結(jié)一下,通過 domain 選項的設置,可以實現(xiàn)允許在子域訪問 cookie。

expires,max-age

默認情況下,如果一個 cookie 沒有設置這兩個參數(shù)中的任何一個,那么在關閉瀏覽器之后,它就會消失。此類 cookie 被稱為 "session cookie”。

為了讓 cookie 在瀏覽器關閉后仍然存在,我們可以設置 expires 或 max-age 選項中的一個。

  • ?expires=Tue, 19 Jan 2038 03:14:07 GMT?

cookie 的過期時間定義了瀏覽器會自動清除該 cookie 的時間。

日期必須完全采用 GMT 時區(qū)的這種格式。我們可以使用 date.toUTCString 來獲取它。例如,我們可以將 cookie 設置為 1 天后過期。

// 當前時間 +1 天
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

如果我們將 expires 設置為過去的時間,則 cookie 會被刪除。

  • ?max-age=3600?

它是 expires 的替代選項,指明了 cookie 的過期時間距離當前時間的秒數(shù)。

如果將其設置為 0 或負數(shù),則 cookie 會被刪除:

// cookie 會在一小時后失效
document.cookie = "user=John; max-age=3600";

// 刪除 cookie(讓它立即過期)
document.cookie = "user=John; max-age=0";

secure

  • ?secure?

Cookie 應只能被通過 HTTPS 傳輸。

默認情況下,如果我們在 http://site.com 上設置了 cookie,那么該 cookie 也會出現(xiàn)在 https://site.com 上,反之亦然。

也就是說,cookie 是基于域的,它們不區(qū)分協(xié)議。

使用此選項,如果一個 cookie 是通過 https://site.com 設置的,那么它不會在相同域的 HTTP 環(huán)境下出現(xiàn),例如 http://site.com。所以,如果一個 cookie 包含絕不應該通過未加密的 HTTP 協(xié)議發(fā)送的敏感內(nèi)容,那么就應該設置 secure 標識。

// 假設我們現(xiàn)在在 HTTPS 環(huán)境下
// 設置 cookie secure(只在 HTTPS 環(huán)境下可訪問)
document.cookie = "user=John; secure";

samesite

這是另外一個關于安全的特性。它旨在防止 XSRF(跨網(wǎng)站請求偽造)攻擊。

為了了解它是如何工作的,以及何時有用,讓我們看一下 XSRF 攻擊。

XSRF 攻擊

想象一下,你登錄了 bank.com 網(wǎng)站。此時:你有了來自該網(wǎng)站的身份驗證 cookie。你的瀏覽器會在每次請求時將其發(fā)送到 bank.com,以便識別你,并執(zhí)行所有敏感的財務上的操作。

現(xiàn)在,在另外一個窗口中瀏覽網(wǎng)頁時,你不小心訪問了另一個網(wǎng)站 evil.com。該網(wǎng)站具有向 bank.com 網(wǎng)站提交一個具有啟動與黑客賬戶交易的字段的表單 <form action="https://bank.com/pay"> 的 JavaScript 代碼。

你每次訪問 bank.com 時,瀏覽器都會發(fā)送 cookie,即使該表單是從 evil.com 提交過來的。因此,銀行會識別你的身份,并執(zhí)行真實的付款。


這就是所謂的“跨網(wǎng)站請求偽造(Cross-Site Request Forgery,簡稱 XSRF)”攻擊。

當然,實際的銀行會防止出現(xiàn)這種情況。所有由 bank.com 生成的表單都具有一個特殊的字段,即所謂的 “XSRF 保護 token”,惡意頁面既不能生成,也不能從遠程頁面提取它。它可以在那里提交表單,但是無法獲取數(shù)據(jù)。并且,網(wǎng)站 bank.com 會對收到的每個表單都進行這種 token 的檢查。

但是,實現(xiàn)這種防護需要花費時間。我們需要確保每個表單都具有所需的 token 字段,并且我們還必須檢查所有請求。

輸入 cookie samesite 選項

Cookie 的 samesite 選項提供了另一種防止此類攻擊的方式,(理論上)不需要要求 “XSRF 保護 token”。

它有兩個可能的值:

  • ?samesite=strict?(和沒有值的 ?samesite? 一樣)

如果用戶來自同一網(wǎng)站之外,那么設置了 samesite=strict 的 cookie 永遠不會被發(fā)送。

換句話說,無論用戶是通過郵件鏈接還是從 evil.com 提交表單,或者進行了任何來自其他域下的操作,cookie 都不會被發(fā)送。

如果身份驗證 cookie 具有 samesite 選項,那么 XSRF 攻擊是沒有機會成功的,因為來自 evil.com 的提交沒有 cookie。因此,bank.com 將無法識別用戶,也就不會繼續(xù)進行付款。

這種保護是相當可靠的。只有來自 bank.com 的操作才會發(fā)送 samesite cookie,例如來自 bank.com 的另一頁面的表單提交。

雖然,這樣有一些不方便。

當用戶通過合法的鏈接訪問 bank.com 時,例如從他們自己的筆記,他們會感到驚訝,bank.com 無法識別他們的身份。實際上,在這種情況下不會發(fā)送 samesite=strict cookie。

我們可以通過使用兩個 cookie 來解決這個問題:一個 cookie 用于“一般識別”,僅用于說 “Hello, John”,另一個帶有 samesite=strict 的 cookie 用于進行數(shù)據(jù)更改的操作。這樣,從網(wǎng)站外部來的用戶會看到歡迎信息,但是支付操作必須是從銀行網(wǎng)站啟動的,這樣第二個 cookie 才能被發(fā)送。

  • ?samesite=lax?

一種更輕松的方法,該方法還可以防止 XSRF 攻擊,并且不會破壞用戶體驗。

寬松(lax)模式,和 strict 模式類似,當從外部來到網(wǎng)站,則禁止瀏覽器發(fā)送 cookie,但是增加了一個例外。

如果以下兩個條件均成立,則會發(fā)送含 samesite=lax 的 cookie:

  1. HTTP 方法是“安全的”(例如 GET 方法,而不是 POST)。
  2. 所有安全的 HTTP 方法詳見 RFC7231 規(guī)范?;旧?,這些都是用于讀取而不是寫入數(shù)據(jù)的方法。它們不得執(zhí)行任何更改數(shù)據(jù)的操作。跟隨鏈接始終是 GET,是安全的方法。

  3. 該操作執(zhí)行頂級導航(更改瀏覽器地址欄中的 URL)。
  4. 這通常是成立的,但是如果導航是在一個 <iframe> 中執(zhí)行的,那么它就不是頂級的。此外,用于網(wǎng)絡請求的 JavaScript 方法不會執(zhí)行任何導航,因此它們不適合。

所以,samesite=lax 所做的是基本上允許最常見的“前往 URL”操作攜帶 cookie。例如,從筆記中打開網(wǎng)站鏈接就滿足這些條件。

但是,任何更復雜的事兒,例如來自另一個網(wǎng)站的網(wǎng)絡請求或表單提交都會丟失 cookie。

如果這種情況適合你,那么添加 samesite=lax 將不會破壞用戶體驗并且可以增加保護。

總體而言,samesite 是一個很好的選項。

但它有個缺點:

  • ?samesite? 會被到 2017 年左右的舊版本瀏覽器忽略(不兼容)。

因此,如果我們僅依靠 samesite 提供保護,那么在舊版本的瀏覽器上將很容易受到攻擊。

但是,我們肯定可以將 samesite 與其他保護措施一起使用,例如 XSRF token,這樣可以多增加一層保護,將來,當舊版本的瀏覽器淘汰時,我們可能就可以刪除 xsrf token 這種方式了。

httpOnly

這個選項和 JavaScript 沒有關系,但是我們必須為了完整性也提一下它。

Web 服務器使用 Set-Cookie header 來設置 cookie。并且,它可以設置 httpOnly 選項。

這個選項禁止任何 JavaScript 訪問 cookie。我們使用 document.cookie 看不到此類 cookie,也無法對此類 cookie 進行操作。

這是一種預防措施,當黑客將自己的 JavaScript 代碼注入網(wǎng)頁,并等待用戶訪問該頁面時發(fā)起攻擊,而這個選項可以防止此時的這種攻擊。這應該是不可能發(fā)生的,黑客應該無法將他們的代碼注入我們的網(wǎng)站,但是網(wǎng)站有可能存在 bug,使得黑客能夠?qū)崿F(xiàn)這樣的操作。

通常來說,如果發(fā)生了這種情況,并且用戶訪問了帶有黑客 JavaScript 代碼的頁面,黑客代碼將執(zhí)行并通過 document.cookie 獲取到包含用戶身份驗證信息的 cookie。這就很糟糕了。

但是,如果 cookie 設置了 httpOnly,那么 document.cookie 則看不到 cookie,所以它受到了保護。

附錄: Cookie 函數(shù)

這里有一組有關 cookie 操作的函數(shù),比手動修改 document.cookie 方便得多。

有很多這種 cookie 庫,所以這些函數(shù)只用于演示。雖然它們都能正常使用。

getCookie(name)

獲取 cookie 最簡短的方式是使用 正則表達式

getCookie(name) 函數(shù)返回具有給定 name 的 cookie:

// 返回具有給定 name 的 cookie,
// 如果沒找到,則返回 undefined
function getCookie(name) {
  let matches = document.cookie.match(new RegExp(
    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
  ));
  return matches ? decodeURIComponent(matches[1]) : undefined;
}

這里的 new RegExp 是動態(tài)生成的,以匹配 ; name=<value>。

請注意 cookie 的值是經(jīng)過編碼的,所以 getCookie 使用了內(nèi)建方法 decodeURIComponent 函數(shù)對其進行解碼。

setCookie(name, value, options)

將 cookie 的 name 設置為具有默認值 path=/(可以修改以添加其他默認值)和給定值 value

function setCookie(name, value, options = {}) {

  options = {
    path: '/',
    // 如果需要,可以在這里添加其他默認值
    ...options
  };

  if (options.expires instanceof Date) {
    options.expires = options.expires.toUTCString();
  }

  let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);

  for (let optionKey in options) {
    updatedCookie += "; " + optionKey;
    let optionValue = options[optionKey];
    if (optionValue !== true) {
      updatedCookie += "=" + optionValue;
    }
  }

  document.cookie = updatedCookie;
}

// 使用范例:
setCookie('user', 'John', {secure: true, 'max-age': 3600});

deleteCookie(name)

要刪除一個 cookie,我們可以給它設置一個負的過期時間來調(diào)用它:

function deleteCookie(name) {
  setCookie(name, "", {
    'max-age': -1
  })
}

更新或刪除必須使用相同的路徑和域

請注意:當我們更新或刪除一個 cookie 時,我們應該使用和設置 cookie 時相同的路徑和域選項。

代碼放在:cookie.js

附錄:第三方 cookie

如果 cookie 是由用戶所訪問的頁面的域以外的域放置的,則稱其為第三方 cookie。

例如:

  1. ?site.com? 網(wǎng)站的一個頁面加載了另外一個網(wǎng)站的 banner:?<img src="https://ads.com/banner.png" rel="external nofollow" >?。
  2. 與 banner 一起,?ads.com? 的遠程服務器可能會設置帶有 ?id=1234? 這樣的 cookie 的 ?Set-Cookie? header。此類 cookie 源自 ?ads.com? 域,并且僅在 ?ads.com? 中可見:

  3. 下次訪問 ads.com 網(wǎng)站時,遠程服務器獲取 cookie id 并識別用戶:

  4. 更為重要的是,當用戶從 site.com 網(wǎng)站跳轉(zhuǎn)至另一個也帶有 banner 的網(wǎng)站 other.com 時,ads.com 會獲得該 cookie,因為它屬于 ads.com,從而識別用戶并在他在網(wǎng)站之間切換時對其進行跟蹤:

由于它的性質(zhì),第三方 cookie 通常用于跟蹤和廣告服務。它們被綁定在原始域上,因此 ads.com 可以在不同網(wǎng)站之間跟蹤同一用戶,如果這些網(wǎng)站都可以訪問 ads.com 的話。

當然,有些人不喜歡被跟蹤,因此瀏覽器允許禁止此類 cookie。

此外,一些現(xiàn)代瀏覽器對此類 cookie 采取特殊策略:

  • Safari 瀏覽器完全不允許第三方 cookie。
  • Firefox 瀏覽器附帶了一個第三方域的黑名單,它阻止了來自名單內(nèi)的域的第三方 cookie。

請注意:

如果我們加載了一個來自第三方域的腳本,例如 <script src="https://google-analytics.com/analytics.js" rel="external nofollow" >,并且該腳本使用 document.cookie 設置了 cookie,那么此類 cookie 就不是第三方的。

如果一個腳本設置了一個 cookie,那么無論腳本來自何處 —— 這個 cookie 都屬于當前網(wǎng)頁的域。

附錄: GDPR

本主題和 JavaScript 無關,只是設置 cookie 時的一些注意事項。

歐洲有一項名為 GDPR 的立法,該法規(guī)針對網(wǎng)站尊重用戶實施了一系列規(guī)則。其中之一就是需要明確的許可才可以跟蹤用戶的 cookie。

請注意,這僅與跟蹤/識別/授權(quán) cookie 有關。

所以,如果我們設置一個只保存了一些信息的 cookie,但是既不跟蹤也不識別用戶,那么我們可以自由地設置它。

但是,如果我們要設置帶有身份驗證會話(session)或跟蹤 id 的 cookie,那么必須得到用戶的允許。

網(wǎng)站為了遵循 GDPR 通常有兩種做法。你一定已經(jīng)在網(wǎng)站中看到過它們了:

  1. 如果一個網(wǎng)站想要僅為已經(jīng)經(jīng)過身份驗證的用戶設置跟蹤的 cookie。
  2. 為此,注冊表單中必須要有一個復選框,例如“接受隱私政策”(描述怎么使用 cookie),用戶必須勾選它,然后網(wǎng)站就可以自由設置身份驗證 cookie 了。

  3. 如果一個網(wǎng)站想要為所有人設置跟蹤的 cookie。
  4. 為了合法地這樣做,網(wǎng)站為每個新用戶顯示一個“初始屏幕”彈窗,并要求他們同意設置 cookie。之后網(wǎng)站就可以設置 cookie,并可以讓用戶看到網(wǎng)站內(nèi)容了。不過,這可能會使新用戶感到反感。沒有人喜歡看到“必須點擊”的初始屏幕彈窗而不是網(wǎng)站內(nèi)容。但是 GDPR 要求必須得到用戶明確地準許。

GDPR 不僅涉及 cookie,還涉及其他與隱私相關的問題,但這超出了我們的討論范圍。

總結(jié)

document.cookie 提供了對 cookie 的訪問

  • 寫入操作只會修改其中提到的 cookie。
  • name/value 必須被編碼。
  • 一個 cookie 最大不能超過 4KB。每個域下最多允許有 20+ 個左右的 cookie(具體取決于瀏覽器)。

Cookie 選項:

  • ?path=/?,默認為當前路徑,使 cookie 僅在該路徑下可見。
  • ?domain=site.com?,默認 cookie 僅在當前域下可見。如果顯式地設置了域,可以使 cookie 在子域下也可見。
  • ?expires? 或 ?max-age? 設定了 cookie 過期時間。如果沒有設置,則當瀏覽器關閉時 cookie 就會失效。
  • ?secure? 使 cookie 僅在 HTTPS 下有效。
  • ?samesite?,如果請求來自外部網(wǎng)站,禁止瀏覽器發(fā)送 cookie。這有助于防止 XSRF 攻擊。

另外:

  • 瀏覽器可能會禁用第三方 cookie,例如 Safari 瀏覽器默認禁止所有第三方 cookie。
  • 在為歐盟公民設置跟蹤 cookie 時,GDPR 要求必須得到用戶明確許可。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號