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,被浏览器保存在本地,以便随时取用。
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 的跨站问题
这里需要额外提一下 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 + 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 属性值对照表
请求类型 | 示例 | Strict | Lax | None |
---|---|---|---|---|
链接 | <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 的各个属性看着很多,不太好记。可以按照文章开头的那样,将其分为几类:
必不可少的属性,这个分类没啥好说的,本质上就是 name=value 的键值对,保存着 Cookie 的值
- Name
- Value
控制 Cookie 作用范围的属性
- Domain
- Path
控制 Cookie 过期时间的属性
- Expires
- MaxAge
控制 Cookie 安全性的属性
- HTTPOnly
- Secure
- SameSite
分类后再结合上面文章进行理解与联想,应该比死记硬背好记多了。
附:Cookie 的各个属性速查
属性 | 说明 |
---|---|
Name | Cookie 名称 |
Value | Cookie 的值 |
Domain | Cookie 所属的域 |
Path | Cookie 所属的路径 |
Expires | Cookie 过期时间,值为一个时间 |
Max-Age | Cookie 过期时间,值为数字,代表过期秒数 Max-Age 优先级比 Expires 更高 |
HTTPOnly | 设置为 HTTPOnly 的 Cookie,无法通过 document.cookie 访问 |
Secure | 标记为 Secure 的 Cookie 应通过 HTTPS 协议发送给服务端 |
SameSite | 控制跨站请求是否携带 Cookie |
参考链接:
- HTTP cookie - Wikipedia
- Using HTTP cookies - HTTP | MDN (mozilla.org)
- Top-level domain - Wikipedia
- Public Suffix List - Wikipedia
- Cookie Expires/Max-Age attribute upper limit - Chrome Platform Status (chromestatus.com)
- Cookies: HTTP State Management Mechanism (httpwg.org)
- HTTP Cookie - HTTP | MDN (mozilla.org)
- Set-Cookie - HTTP | MDN (mozilla.org)
- 浏览器系列之 Cookie 和 SameSite 属性 · Issue #157 · mqyqingfeng/Blog (github.com)