2024年2月13日

Cookies,document.cookie

Cookies 是存储在浏览器中的少量数据字符串。它们是 HTTP 协议的一部分,由 RFC 6265 规范定义。

Cookies 通常由 Web 服务器使用响应 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"

domain

  • domain=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 从所有网站页面中可访问。如果未设置此属性,则默认值将使用 此方法 计算。

expires, max-age

默认情况下,如果 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";

secure

  • secure

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 攻击,并且不会破坏用户体验。

与 `strict` 模式一样,Lax 模式禁止浏览器在从网站外部访问时发送 Cookie,但增加了一个例外。

如果以下两个条件都满足,则会发送 `samesite=lax` Cookie

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

    安全 HTTP 方法的完整列表在 RFC7231 规范 中。这些方法应该用于读取数据,但不应该用于写入数据。它们不能执行任何数据更改操作。通过链接访问始终是 GET 方法,即安全方法。

  2. 操作执行顶级导航(更改浏览器地址栏中的 URL)。

    这通常是正确的,但如果导航是在 `<iframe>` 中执行的,那么它就不是顶级导航。此外,用于网络请求的 JavaScript 方法不会执行任何导航。

因此,`samesite=lax` 所做的是允许最常见的“转到 URL”操作使用 Cookie。例如,从满足这些条件的笔记中打开网站链接。

但任何更复杂的操作,例如来自另一个网站的网络请求或表单提交,都会丢失 Cookie。

如果这对您来说没问题,那么添加 `samesite=lax` 可能不会破坏用户体验,并能提供保护。

总的来说,`samesite` 是一个很棒的属性。

存在一个缺点

  • samesite 被非常旧的浏览器(大约 2017 年之前的浏览器)忽略(不支持)。

因此,如果我们仅仅依靠 samesite 来提供保护,那么旧浏览器将容易受到攻击。

但是,我们可以将 samesite 与其他保护措施(如 xsrf 令牌)一起使用,以增加一层防御。将来,当旧浏览器逐渐淘汰后,我们可能能够放弃 xsrf 令牌。

httpOnly

此属性与 JavaScript 无关,但为了完整性,我们必须提及它。

Web 服务器使用 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.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 的变体。您很可能在网上都见过它们。

  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 问题或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用<code>标签,对于多行代码,请使用<pre>标签,对于超过 10 行的代码,请使用沙盒(plnkrjsbincodepen…)