# Cookie,document.cookie Cookie 是直接存储在浏览器中的一小串数据。它们是 HTTP 协议的一部分,由 [RFC 6265](https://tools.ietf.org/html/rfc6265) 规范定义。 Cookie 通常是由 Web 服务器使用响应 `Set-Cookie` HTTP-header 设置的。然后浏览器使用 `Cookie` HTTP-header 将它们自动添加到(几乎)每个对相同域的请求中。 最常见的用处之一就是身份验证: 1. 登录后,服务器在响应中使用 `Set-Cookie` HTTP-header 来设置具有唯一“会话标识符(session identifier)”的 cookie。 2. 下次当请求被发送到同一个域时,浏览器会使用 `Cookie` HTTP-header 通过网络发送 cookie。 3. 所以服务器知道是谁发起了请求。 我们还可以使用 `document.cookie` 属性从浏览器访问 cookie。 关于 cookie 及其选项,有很多棘手的事情。在本章中,我们将详细介绍它们。 ## 从 document.cookie 中读取 ```online 你的浏览器是否存储了本网站的任何 cookie?让我们来看看: ``` ```offline 假设你在一个网站上,则可以看到来自该网站的 cookie,像这样: ``` ```js run // 在 javascript.info,我们使用谷歌分析来进行统计, // 所以应该存在一些 cookie alert( document.cookie ); // cookie1=value1; cookie2=value2;... ``` `document.cookie` 的值由 `name=value` 对组成,以 `; ` 分隔。每一个都是独立的 cookie。 为了找到一个特定的 cookie,我们可以以 `; ` 作为分隔,将 `document.cookie` 分开,然后找到对应的名字。我们可以使用正则表达式或者数组函数来实现。 我们把这个留给读者当作练习。此外,在本章的最后,你可以找到一些操作 cookie 的辅助函数。 ## 写入 document.cookie 我们可以写入 `document.cookie`。但这不是一个数据属性,它是一个 [访问器(getter/setter)](info:property-accessors)。对其的赋值操作会被特殊处理。 **对 `document.cookie` 的写入操作只会更新其中提到的 cookie,而不会涉及其他 cookie。** 例如,此调用设置了一个名称为 `user` 且值为 `John` 的 cookie: ```js run document.cookie = "user=John"; // 只会更新名称为 user 的 cookie alert(document.cookie); // 展示所有 cookie ``` 如果你运行了上面这段代码,你会看到多个 cookie。这是因为 `document.cookie=` 操作不是重写整所有 cookie。它只设置代码中提到的 cookie `user`。 从技术上讲,cookie 的名称和值可以是任何字符。为了保持有效的格式,它们应该使用内建的 `encodeURIComponent` 函数对其进行转义: ```js run // 特殊字符(空格),需要编码 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 ``` ```warn header="限制" 存在一些限制: - `encodeURIComponent` 编码后的 `name=value` 对,大小不能超过 4KB。因此,我们不能在一个 cookie 中保存大的东西。 - 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器。 ``` Cookie 有几个选项,其中很多都很重要,应该设置它。 选项被列在 `key=value` 之后,以 `;` 分隔,像这样: ```js run 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 对此网站的所有页面可见。 ## domain - **`domain=site.com`** domain 控制了可访问 cookie 的域。但是在实际中,有一些限制。我们无法设置任何域。 **无法从另一个二级域访问 cookie,因此 `other.com` 永远不会收到在 `site.com` 设置的 cookie。** 这是一项安全限制,为了允许我们将敏感数据存储在应该仅在一个站点上可用的 cookie 中。 默认情况下,cookie 只有在设置的域下才能被访问到。 请注意,默认情况下,cookie 也不会共享给子域,例如 `forum.site.com`。 ```js // 如果我们在 site.com 网站上设置了 cookie…… document.cookie = "user=John" // ……在 forum.site.com 域下我们无法访问它 alert(document.cookie); // 没有 user ``` ……但这是可以设置的。如果我们想允许像 `forum.site.com` 这样的子域在 `site.com` 上设置 cookie,也是可以实现的。 为此,当在 `site.com` 设置 cookie 时,我们应该明确地将 `domain` 选项设置为根域:`domain=site.com`。那么,所有子域都可以访问到这样的 cookie。 例如: ```js // 在 site.com // 使 cookie 可以被在任何子域 *.site.com 访问: document.cookie = "user=John; *!*domain=site.com*/!*" // 之后 // 在 forum.site.com alert(document.cookie); // 有 cookie user=John ``` 出于历史原因,`domain=.site.com`(`site.com` 前面有一个点符号)也以相同的方式工作,允许从子域访问 cookie。这是一个旧的表示方式,如果我们需要支持非常旧的浏览器,那么应该使用它。 总结一下,通过 `domain` 选项的设置,可以实现允许在子域访问 cookie。 ## expires,max-age 默认情况下,如果一个 cookie 这两个参数都没有设置,那么在关闭浏览器之后,它就会消失。此类 cookie 被称为 "session cookie”。 为了让 cookie 在浏览器关闭后仍然存在,我们可以设置 `expires` 或 `max-age` 选项中的一个。 - **`expires=Tue, 19 Jan 2038 03:14:07 GMT`** cookie 的过期时间定义了浏览器会自动清除该 cookie 的时间。 日期必须完全采用 GMT 时区的这种格式。我们可以使用 `date.toUTCString` 来获取它。例如,我们可以将 cookie 设置为 1 天后过期。 ```js // 当前时间 +1 天 let date = new Date(Date.now() + 86400e3); date = date.toUTCString(); document.cookie = "user=John; expires=" + date; ``` 如果我们将 `expires` 设置为过去的时间,则 cookie 会被删除。 - **`max-age=3600`** 它是 `expires` 的替代选项,指明了 cookie 的过期时间距离当前时间的秒数。 如果将其设置为 0 或负数,则 cookie 会被删除: ```js // 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 也会出现在 `https://site.com` 上,反之亦然。** 也就是说,cookie 是基于域的,它们不区分协议。 使用此选项,如果一个 cookie 是通过 `https://site.com` 设置的,那么它不会在相同域的 HTTP 环境下出现,例如 `http://site.com`。所以,如果一个 cookie 包含绝不应该通过未加密的 HTTP 协议发送的敏感内容,那么就应该设置 `secure` 标识。 ```js // 假设我们现在在 HTTPS 环境下 // 设置 cookie secure(只在 HTTPS 环境下可访问) document.cookie = "user=John; secure"; ``` ## samesite 这是另外一个关于安全的特性。它旨在防止 XSRF(跨网站请求伪造)攻击。 为了了解它是如何工作的,以及何时有用,让我们看一下 XSRF 攻击。 ### XSRF 攻击 想象一下,你登录了 `bank.com` 网站。此时:你有了来自该网站的身份验证 cookie。你的浏览器会在每次请求时将其发送到 `bank.com`,以便识别你,并执行所有敏感的财务上的操作。 现在,在另外一个窗口中浏览网页时,你不小心访问了另一个网站 `evil.com`。该网站具有向 `bank.com` 网站提交一个具有启动与黑客账户交易的字段的表单 `
` 的 JavaScript 代码。 你每次访问 `bank.com` 时,浏览器都会发送 cookie,即使该表单是从 `evil.com` 提交过来的。因此,银行会识别你的身份,并执行真实的付款。 ![](cookie-xsrf.svg) 这就是所谓的“跨网站请求伪造(Cross-Site Request Forgery,简称 XSRF)”攻击。 当然,实际的银行会防止出现这种情况。所有由 `bank.com` 生成的表单都具有一个特殊的字段,即所谓的 “XSRF 保护 token”,恶意页面既不能生成,也不能从远程页面提取它。它可以在那里提交表单,但是无法获取数据。并且,网站 `bank.com` 会对收到的每个表单都进行这种 token 的检查。 但是,实现这种防护需要花费时间。我们需要确保每个表单都具有所需的 token 字段,并且我们还必须检查所有请求。 ### 输入 cookie samesite 选项 Cookie 的 `samesite` 选项提供了另一种防止此类攻击的方式,(理论上)不需要要求 “XSRF 保护 token”。 它有两个可能的值: - **`samesite=strict`(和没有值的 `samesite` 一样)** 如果用户来自同一网站之外,那么设置了 `samesite=strict` 的 cookie 永远不会被发送。 换句话说,无论用户是通过邮件链接还是从 `evil.com` 提交表单,或者进行了任何来自其他域下的操作,cookie 都不会被发送。 如果身份验证 cookie 具有 `samesite` 选项,那么 XSRF 攻击是没有机会成功的,因为来自 `evil.com` 的提交没有 cookie。因此,`bank.com` 将无法识别用户,也就不会继续进行付款。 这种保护是相当可靠的。只有来自 `bank.com` 的操作才会发送 `samesite` cookie,例如来自 `bank.com` 的另一页面的表单提交。 虽然,这样有一些不方便。 当用户通过合法的链接访问 `bank.com` 时,例如从他们自己的笔记,他们会感到惊讶,`bank.com` 无法识别他们的身份。实际上,在这种情况下不会发送 `samesite=strict` cookie。 我们可以通过使用两个 cookie 来解决这个问题:一个 cookie 用于“一般识别”,仅用于说 "Hello, John",另一个带有 `samesite=strict` 的 cookie 用于进行数据更改的操作。这样,从网站外部来的用户会看到欢迎信息,但是支付操作必须是从银行网站启动的,这样第二个 cookie 才能被发送。 - **`samesite=lax`** 一种更轻松的方法,该方法还可以防止 XSRF 攻击,并且不会破坏用户体验。 宽松(lax)模式,和 `strict` 模式类似,当从外部来到网站,则禁止浏览器发送 cookie,但是增加了一个例外。 如果以下两个条件均成立,则会发送含 `samesite=lax` 的 cookie: 1. HTTP 方法是“安全的”(例如 GET 方法,而不是 POST)。 所有安全的 HTTP 方法详见 [RFC7231 规范](https://tools.ietf.org/html/rfc7231)。基本上,这些都是用于读取而不是写入数据的方法。它们不得执行任何更改数据的操作。跟随链接始终是 GET,是安全的方法。 2. 该操作执行顶级导航(更改浏览器地址栏中的 URL)。 这通常是成立的,但是如果导航是在一个 `