Cookies 是存储在浏览器中的少量数据字符串。它们是 HTTP 协议的一部分,由 RFC 6265 规范定义。
Cookies 通常由 Web 服务器使用响应 Set-Cookie
HTTP 标头设置。然后,浏览器会自动将它们添加到(几乎)对同一域名的每个请求中,使用 Cookie
HTTP 标头。
最广泛的用例之一是身份验证
- 登录后,服务器使用响应中的
Set-Cookie
HTTP 标头设置一个包含唯一“会话标识符”的 cookie。 - 下次向同一域名发送请求时,浏览器会使用
Cookie
HTTP 标头通过网络发送 cookie。 - 因此服务器知道是谁发出了请求。
我们也可以使用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 在浏览器关闭后仍然存在,我们可以设置 expires
或 max-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
-
HTTP 方法是“安全的”(例如 GET,但不是 POST)。
安全 HTTP 方法的完整列表在 RFC7231 规范 中。这些方法应该用于读取数据,但不应该用于写入数据。它们不能执行任何数据更改操作。通过链接访问始终是 GET 方法,即安全方法。
-
操作执行顶级导航(更改浏览器地址栏中的 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。
例如
-
site.com
上的页面从另一个站点加载横幅:<img src="https://ads.com/banner.png">
。 -
除了横幅之外,
ads.com
上的远程服务器可能会设置Set-Cookie
标头,其中包含一个像id=1234
这样的 cookie。此类 cookie 来自ads.com
域,并且仅在ads.com
上可见。 -
下次访问
ads.com
时,远程服务器会获取id
cookie 并识别用户。 -
更重要的是,当用户从
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 的变体。您很可能在网上都见过它们。
-
如果一个网站想要只为已认证的用户设置跟踪 Cookie。
为此,注册表单应该有一个类似“接受隐私政策”的复选框(其中描述了 Cookie 的使用方式),用户必须勾选它,然后网站就可以自由设置身份验证 Cookie。
-
如果一个网站想要为所有人设置跟踪 Cookie。
为了合法地做到这一点,网站会为新用户显示一个模态“启动画面”,并要求他们同意 Cookie。然后网站就可以设置 Cookie 并让人们看到内容。但这可能会让新访客感到困扰。没有人喜欢看到这种“必须点击”的模态启动画面而不是内容。但 GDPR 要求明确同意。
GDPR 不仅仅与 Cookie 有关,它还与其他与隐私相关的问题有关,但这超出了我们的范围。
总结
document.cookie
提供对 Cookie 的访问。
- 写入操作只会修改其中提到的 Cookie。
- 名称/值必须进行编码。
- 一个 Cookie 的大小不能超过 4KB。一个域允许的 Cookie 数量大约为 20 个以上(因浏览器而异)。
Cookie 属性
path=/
,默认情况下为当前路径,使 Cookie 仅在该路径下可见。domain=site.com
,默认情况下,Cookie 仅在当前域可见。如果显式设置了域,则 Cookie 将在子域可见。expires
或max-age
设置 Cookie 过期时间。如果没有它们,Cookie 在浏览器关闭时就会失效。secure
使 Cookie 仅限 HTTPS。samesite
禁止浏览器将 Cookie 与来自网站外部的请求一起发送。这有助于防止 XSRF 攻击。
此外
- 浏览器可能会禁止第三方 Cookie,例如 Safari 默认情况下会这样做。Chrome 也正在进行相关工作以实现这一点。
- 为欧盟公民设置跟踪 Cookie 时,GDPR 要求征得许可。
评论
<code>
标签,对于多行代码,请使用<pre>
标签,对于超过 10 行的代码,请使用沙盒(plnkr,jsbin,codepen…)