Cookie 的那些事——深入理解Cookie

本来这篇文章是准备结合 HTTP 一起讲讲 Cookie 的那点事,赶在 10.24 写完的。

不过我明显高估了自己遣词造句的速度……为了完成它,不得已把 HTTP 的部分砍了,虽然本来也只是打算讲讲 HTTP 的诞生和兴起,转而引入 Cookie 的出现。

即使把开头的部分砍掉了,这篇文章依然拖到了 25 号上午才彻底完成,好吧 🙄

前言

HTTP 设计之初就是无状态的协议,它无法识别前后两次请求是否为同一个客户端。试想一下:你登录了一个网站,结果下次请求时服务端认不出你,让你再次登录,你又登录了网站,结果……哈,这可不妙。

在需要保持 HTTP 协议无状态特性的同时,又要解决类似的问题,在这一背景下,Cookie 技术出现了。

它的原理非常简单,当服务端想要设置一个状态时,会添加一个叫做 Set-Cookie 的响应头部通知客户端保存 Cookie。当客户端下次再向服务器发送请求时,会将 Cookie 带上,服务端会根据发送的 Cookie 获取到之前保存的状态信息。

这时候登录的流程就变成了: 用户登录成功了一个网站,服务端返回响应的同时设置了 Set-Cookie 响应头,通知客户端将能识别用户的一串信息保存下来,下次客户端再次发送请求,会自动把 Cookie 带上,借助 Cookie 中保存的信息,服务端成功知晓了这是哪个用户。

知道了 Cookie 的实现原理,再来看看 Cookie 的各种属性。

Cookie 本质上是小型的文本文件,根据过期时间,既可以是会话 Cookie,关闭浏览器即时失效;也可以是持久化 Cookie,被浏览器保存在本地,以便随时取用。

Cookie 主要由 Name, Value 以及其他几个用于控制 Cookie 作用范围,过期时间和安全性的属性组成,在现代浏览器中其大小一般不超过 4KB。

Cookie 的各个属性的设置语法如下:

Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure

// 也可以同时设置多种属性
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

Name 和 Value

Name 和 Value 属性用于表示 Cookie 的名字和值。服务端可以在响应中使用多个头部字段来设置 Cookie,同时这里要注意特殊字符会被转义。

Set-Cookie: theme=daylihgt
Set-Cookie: sessionToken=abc123

Domain 和 Path

Domain 和 Path 用来控制 Cookie 的作用范围。

Domain 属性指定哪些主机可以接受 Cookie,出于安全方面的考虑,只能设置为当前的顶级域或者子域,不能设置为其他域名(比如说,当前请求的域名是 qq.com,你不能在 Set-Cookie 中将 Domain 设置为 taobao.com,离谱)如果不显式设置,则默认为设置当前主机,不包含子域名。

备注: 当前大多数浏览器遵循 RFC 6265 ,设置 Domain 时 不需要加前导点。若浏览器不遵循该规范,则需要加前导点,例如:Domain=.mozilla.org

Path 用于指定 Cookie 只对哪个路径生效,同时也会包含子路径。 比如:Set-Cookie: sessionToken=abc123; Path=/API​,那么下面的地址都会被匹配:/API/users​,/API/users/10000​,/API/books​,/API/orders​。

这里需要额外提一下 Cookie 的跨站问题。

首先,问个问题,什么叫做跨站?你可能会想,hai,不是同一个站点不就是跨站么…… a.com,b.com 很明显,跨站了嘛。 那再问个问题,游览器又是根据什么去确认是否同站呢?a.jd.com,b.jd.com 是同站吗?a.eu.org 和 b.eu.org 是同站吗?

答案留到后面再说~~(别急着打我~~

先说说是否同站的判断规则,可以简记为:eTLD + 1 相同即可,具体意思下面会解释。

我们知道 TLD(Top-level Domain) 是顶级域名,比如我们常见的 .com, .org, .net 等等等。理想情况下,我们直接对比二级域名是否相同就能判断是否同站了。但是谁让世界比较复杂呢,试想这么一种情况:某二级域名由域名注册商控制,他最终再给互联网用户分配三级域名,如:a.eu.org, b.eu.org。这时候两个域名的所有者完全是不同的主体,如果再将这两个域名视为同站,很明显会带来潜在的安全问题。

为了解决这个问题,Mozilla 基金会维护了一个 公共后缀列表(Public Suffix List) ,列表中的条目也称为 eTLD(effective top-level domains)。公共后缀列表旨在枚举出所有由域名注册商控制的域名后缀,比如:co.uk,eu.org,github.io 等等等。

eTLD &amp; eTLD+1

这时候就可以解释上面的 eTLD + 1 是什么意思了,即 eTLD 从后再往前进一位。比如:co.uk 是 eTLD,那么 example.co.uk 是 eTLD + 1,eTLD + 1 相同的可以判断为同站,所以 a.example.co.uk 和 b.example.co.uk 是同站。后面的 eu.org 和 github.io 以此类推,都是同样的道理。

看完后,相信已经可以回答上面的问题了,a.jd.com 和 b.jd.com 是同站,而 a.eu.org 和 b.eu.org 不是同站(eu.org 为域名注册商),跨站了。

自有域名以及比较知名的域名很好判断同站跨站,而遇到不确定的二级域名乃至三级域名,就可以去 公共后缀列表(Public Suffix List) 查询,确定该域名是否是由域名注册商控制,它会不会向互联网用户分发下一级的域名。

总之记住同站判断规则,eTLD + 1 相同

Expires 和 Max-Age

Expires 和 Max-Age 用来控制 Cookie 的过期时间。如果不设置,默认为会话 Cookie,关闭浏览器窗口后 Cookie 即失效。

Expires 和 Max-Age 的单位不同,Expires 的值是一个具体的时间。比如:

Set-Cookie: user_id=5; Expires=Fri, 5 Oct 2022 14:28:00 GMT

而 Max-Age 的值,是一个数字,代表秒数,指定的秒数后,Cookie 过期。比如:

Set-Cookie: sessionId=10096; Max-Age=86400

这里需要注意:Max-Age 的值可以为正数,负数或者 0。 正数就是过期秒数;如果是负数,代表是会话性 Cookie;如果为 0,会删除该 Cookie。

如果 Expires 和 Max 两者同时被设置了,Max-Age 优先级更高,Expires 会被忽略。

目前(2022-10-25),IETF 的 HTTP 工作组有 一项草案 ,旨在为 Expires 和 Max-Age 设置上限,它要求用户代理必须限制 Expires 和 Max-Age 属性的最大值不超过 400 天(即 34560000 秒)。Chrome 目前已经 跟进了这项工作

最后顺带一提,这里可以和 HTTP 头部字段中的 Expires 和 Max-Age 结合进行辅助记忆,如果还不清楚这两个是什么,可以忽略这句话。

HTTPOnly

为了安全

HTTPOnly 属性主要用于缓解跨站脚本攻击(XSS),设置为 HTTPOnly 的 Cookie 无法通过 document.cookie 的方式进行访问。例如:

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2022 07:28:00 GMT; HttpOnly

相应的,也无法使用 document.cookie 的方式设置 Cookie 为 HTTPOnly。

Secure

还是为了安全

Secure 属性主要是为了预防中间人攻击,标记为 Secure 的 Cookie 只应通过 HTTPS 协议发送给服务端,并且从 Chrome 52 和 Firefox 52 开始,HTTP 站点无法将 Cookie 标记为 Secure。想设置为 Secure?请使用更安全的 HTTPS 协议。

SameSite

依然是为了安全。

SameSite 属性主要是为了防止跨站请求伪造攻击(CSRS)。 SameSite 可以控制跨站请求时是否携带 Cookie,它有三个取值:None, Lax, Strict,在之前,它的默认取值为 None,表示跨站情况下也会发送 Cookie。

随着 Chrome 80 将 SameSite 设置为 Lax,多数主流浏览器都正将 SameSite 的默认值迁移到 Lax。 *

这很可能会给之前的站点带来一些问题,一个简单修复方式就是显式将 SameSite 设置为 None。但是,如果你想将 SameSite 设置为 None,那么 Cookie 也必须设置为 Secure —— 这意味着网站必须使用 HTTPS 协议(参见上面的 Secure 属性)。

SameSite 设置为 None, Lax, Strict 各个行为的区别,见下面这个 SameSite 属性值对照表:

Cookie 的 SameSite 属性值对照表

请求类型示例StrictLaxNone
链接<a href="..."></a>​​不发送发送 Cookie发送 Cookie
预加载<link rel="prerender" href="..."/>​​不发送发送 Cookie发送 Cookie
GET 表单<form method="GET" action="...">​​不发送发送 Cookie发送 Cookie
POST 表单<form method="POST" action="...">​​不发送不发送发送 Cookie
iframe<iframe src="..."></iframe>​​不发送不发送发送 Cookie
AJAX$.get("...")​​不发送不发送发送 Cookie
Image<img src="...">​​不发送不发送发送 Cookie

总结

Cookie 的各个属性看着很多,不太好记。可以按照文章开头的那样,将其分为几类:

  1. 必不可少的属性,这个分类没啥好说的,本质上就是 name=value 的键值对,保存着 Cookie 的值

    1. Name
    2. Value
  2. 控制 Cookie 作用范围的属性

    1. Domain
    2. Path
  3. 控制 Cookie 过期时间的属性

    1. Expires
    2. MaxAge
  4. 控制 Cookie 安全性的属性

    1. HTTPOnly
    2. Secure
    3. SameSite

分类后再结合上面文章进行理解与联想,应该比死记硬背好记多了。

附:Cookie 的各个属性速查

属性说明
NameCookie 名称
ValueCookie 的值
DomainCookie 所属的域
PathCookie 所属的路径
ExpiresCookie 过期时间,值为一个时间
Max-AgeCookie 过期时间,值为数字,代表过期秒数
Max-Age 优先级比 Expires 更高
HTTPOnly设置为 HTTPOnly 的 Cookie,无法通过 document.cookie​ 访问
Secure标记为 Secure 的 Cookie 应通过 HTTPS 协议发送给服务端
SameSite控制跨站请求是否携带 Cookie

参考链接:

  1. HTTP cookie - Wikipedia
  2. Using HTTP cookies - HTTP | MDN (mozilla.org)
  3. Top-level domain - Wikipedia
  4. Public Suffix List - Wikipedia
  5. Cookie Expires/Max-Age attribute upper limit - Chrome Platform Status (chromestatus.com)
  6. Cookies: HTTP State Management Mechanism (httpwg.org)
  7. HTTP Cookie - HTTP | MDN (mozilla.org)
  8. Set-Cookie - HTTP | MDN (mozilla.org)
  9. 浏览器系列之 Cookie 和 SameSite 属性 · Issue #157 · mqyqingfeng/Blog (github.com)