本文旨在对当前 HTTP Cookie 的运作机制的进行简要梳理。基于 RFC6265 与 HTTP Cookie | MDN
本文假设读者已经对 Cookie 有了大致的了解,但是对于规范中一些细节的把握不充分,因此对部分重要细节进行强调。
内容包括:
- Cookie 机制运作的基本方式
- Cookie 各种属性要求
- Cookie NamePrefix 机制
- Cookie 的一些限制及替代
内容不包括:
- Cookie 使用的场景分析
- Cookie 发展历程及各类安全隐患
- Cookie 的替代方案
基本介绍#
Cookie 提供了一种自动化的状态存储机制。当客户端向服务器发送请求时,服务器可以在响应中携带状态,客户端在后续符合要求的请求中自动携带上该状态。便于服务端进行识别。
典型场景如会话管理、用户偏好、跟踪用户行为。
简单来说,Server 在 HTTP 响应中包含 Set-Cookie Header,UA 会按照要求 Set-Cookie 的属性进行基本的校验并存储到本地(校验失败的会被丢弃),在后续请求中,UA 会根据发起请求的上下文决定是否通过 Cookie Header 自动携带上状态信息。
Cookie Metadata#
设置 Cookie 时可以通过一些 metadata 来控制 Cookie 的有效期和生效范围,分为 Attribute 和 NamePrefix 两种。
具体包括:
有效期控制:
Expires: 过期时间点。Max-Age: Cookie 的最大存活时间,单位为秒。
范围控制:
Domain: 生效域。Set-Cookie 时Path: 生效路径。UA 判断是否携带 Cookie 的因素之一。
特殊标记:
Secure: 仅对 HTTPS 连接生效。HttpOnly: Cookie 作为状态存储在 UA 中时,只能通过 UA 的 HTTP 协议自动
访问,不能通过编程方式访问(无法通过 document.cookie 获取)。SameSite: Cookie 的同源策略,用于防止 CSRF 攻击。Partitioned: Cookie 的分区策略,用于防止跨站点跟踪。
Cookie 在客户端的存储#
Cookie 在 UA 中通过 (Name, Domain, Path) 的三元组来区分 Cookie。
如 (session, example.com, /), (session, api.example.com, /) 会被视为不同的 cookie,在请求时可能会带上具有相同 Name 的 Cookie。
生效期,Expires 与 Max-Age#
Expires 指定 Cookie 过期的具体时间点。
示例:Set-Cookie: sessionToken=abc123; Expires=Wed, 9 Jun 2021 10:18:14 GMT。
Max-Age 指定 Cookie 的最大存活时间,单位为秒。
示例:Set-Cookie: sessionToken=abc123; Max-Age=3600
两者同时存在时 Max-Age 优先级高于 Expires。
当存在两个字段中的任意一种时,该 Cookie 称为持久化 Cookie,过期后才会被清除(过期前也可能清除)。相对的,两个字段都不存在时,称为会话 Cookie,在 UA 被关闭后便会被清除。
Cookie Domain#
Set-Cookie:
根据 Req Host 与 Domain 进行比对校验,判断是否允许该设置 Cookie。
- Domain 可以为空。这种 Cookie 被称为 HostOnly,只有当后续请求与本次请求的 Domain 完全一致时才会携带。
- Domain 需要是 Req Host 或者 Req Host 的父域名(但最多是
eTLD+1)。
设置之后,Cookie 对站点可见。
Cookie:在后续请求中,按照 Req Host 与 Domain 的关系判断是否携带该 Cookie。Req Host 是 Domain 本身或其子域则允许携带。不同 Cookie 之间不干扰。即使多条符合要求的 Cookie 中存在相同的 Key,发送时也会全部发送(Cookie 允许重复)
Cookie Path#
Set-Cookie:无限制,只需要是合法的 urlPath 即可通过校验。
Cookie: 当请求目标 Path 是 Cookie Path 或其子路径,即可携带。
Cookie Secure#
Set-Cookie:无限制。
Cookie:当请求的协议为HTTPS时才允许携带。
Cookie HTTPOnly#
Set-Cookie:无限制。
Cookie: 无限制。
当 Cookie 存储在 UA 中时,由浏览器保证带有该属性的 Cookie 不能被 document.cookie 的方式编程访问。只有浏览器在发送 Req 时按需使用。
SameSite#
Set-Cookie:Secure 属性必须设置,否则忽略。
Cookie: 将当前页面的 Host(地址栏中的 Host)与请求的目标 Host 进行对比。
SameSite 的可选值有三个Strict,Lax,None。
Strict: 将当前页面的 Host(地址栏中的 Host)与请求的目标 Host 进行同站比对,相同才可以发送。Lax:相比Strict更宽松,多一个例外情况(在页面跳转时保持 Cookie):
- 当前请求是进行页面导航:浏览器地址栏中显示的 URL 会发生变化。
- 且当前请求是安全的
HTTP方法: 如GET、HEAD、OPTIONS。
None:不进行同站判定。
Lax 是不设置 SameSite 时的默认值,但是相比显式设置的 SameSite=Lax 更为宽松,在Set-Cookie设置的 120s 内,进行 POST 请求也会一并发送 Cookie。
同站判定标准如下:
-
- 协议必须相同,如
https与http不为同站。
- 协议必须相同,如
-
eTLD + 1相同。
eTLD + 1 示例:
example.com与fe.example.com与api.example.com互为同站,它们的eTLD + 1均为example.com。user1.github.io与user2.github.io不为同站,因为两者的eTLD为github.io,而eTLD + 1分别为user1.github.io与user2.github.io
Cookie Partitioned#
https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies
Set-Cookie:Secure 属性必须设置,否则忽略。建议与 NamePrefix: __Host-/__Host-HTTP- 一同使用。
对于普通的 Cookie,存储时仅将 (Domain,Name,Path),作为区分。
而分区 Cookie 将设置 Cookie 时访问页面的 Domain 也纳入考虑。
变成了:(ContextDomain, Domain,Name,Path)。
Cookie: 在原有基础上,新增 ContextDomain 与当前页面进行比较,只有当当前页面的 Domain 是 ContextDomain 或其子域的情况下才会携带。
NamePrefix#
相比于 Attribute 方案,NamePrefix 虽然有些丑陋,但是却有一些额外的好处。
通过 NamePrefix,将一些 Flag 与 Cookie Name 完全绑定在一起,让浏览器本身能确保某个 Cookie Name 一定启用了某些特性,增强 Set-Cookie 的限制。从而在 UA 端阻止一些意料之外的第三方写入(比如 evil.example.com 可以通过 Domain: example.com 写入 Cookie,通过 __Host 保证 evil.example.com 不能写入 example.com)。
-
__Secure: 连接必须为 HTTPS,且其中有设置
Secure -
__Http: 连接必须为 HTTPS,且其中有设置
Secure与HTTPOnly -
__Host: 浏览器保证 Name 字段不被其他子域设置。(条件:连接必须为
HTTPS,且其中有设置SecureAttr,Path 为/,不设置Domain) -
__Host - Http: 连接必须为 HTTPS,且其中有设置
Secure与HTTPOnly。且浏览器保证 Name 字段不被其他子域设置。
值得注意的是,这些限制从职责分离的角度看用 Attr 似乎更好,但是这需要大改校验逻辑。
比如 __Host:
- 假设
example.com写入了一个带有HostOnlyAttr。 evil.example.com试图写入Domain: example.com(在之前的标准中这个写入请求是合法的)。- 因此为了阻止这个写入, 浏览器必须遍历到这条用于
example.com带有HostOnly的 Cookie,发现出现冲突 - 阻止
evil.example.com的这次写入。
这种方式无法断绝其他潜在的污染路径,并不合算,而 NamePrefix 这种限制可以完全保证 Name一定启用了某些 Flag,用更为抽象一点的说法是:从 Name + Attr 的语义约束升级为 __Flag-Name 的语法约束,保证其不变性。
第三方 Cookie 限制#
当前页面与请求资源的不属于同一个 eTLD + 1 时,此时 Server 试图 Set-Cookie 被称为第三方 Cookie。
上述 Cookie 的各种属性都是从服务端的角度进行状态保持,保证用户体验与安全。但是 Server 完全可以利用对请求携带 Cookie 的弱限制,做到不同站点之间的状态保持,实现用户预期之外的追踪功能。因此为照顾用户利益,浏览器也会对 Server Set-Cookie 的行为进行主动限制,包括阻断写入、修改写入属性等。
如果用户启用了第三方 Cookie 限制,那么这个 Cookie 即使从设置规则上而言是有效的,但是仍可能会被丢弃 / 被修改。
一些典型场景如下:
- 在用户开启阻断第三方 Cookie 的情况下,浏览器可能会拒绝写入 Cookie。
- 在默认情况下,一些浏览器可能会强行为 Set-Cookie 添加 Partitioned Attr。
- 一些写入 Cookie 可能根据 Context 不同导致生命周期缩短,如 Safari 按照不同情况强制
Max-Age=24h/7d - 隐式清理,浏览器可能分析服务端的
Set-Cookie行为。即使成功设置,一段时间后发现异常也会及时清理。
其他#
Cookie 的存储与驱逐限制#
Cookie 作为 HTTP Header,大小有一定的限制。一般实现为 4KB。
-
每个域名下的 Cookie 数量限制在 50-180 不等
-
当 Cookie 触及数量 / 容量限制时就需要进行驱逐,通常采用 LRU 策略。
Cookie 编码#
Cookie 使用除 (0-31, 127) 对应的控制字符外的 US - ASCII。一些实现中会对 Cookie 进行 percent-encoding,有助于遵循字符集限制。
Cookie 不足#
作为一个早期客户端的自动化状态机制,Cookie 提供的一定的便捷性,但也为用户带来了一定的安全困扰。其运作机制还存在一些显著不足。如:对于端口不进行区分。
不完善的 Cookie 机制使得在部分场景下没那么好用,比如 Spotify 的 Embed,通常作为第三方 iframe 嵌入其他页面。为了保持用户信息,Cookie 的用处不大。Spotify 转而使用 Custom Header 和 SessionStorage 进行状态保持。
自定义 Header#
出于安全考虑,Browser 会禁止手动设置部分特殊请求头(称为 "Forbidden Header Names"),这些 Header 通常只能由用户代理(即浏览器)控制。即使设置也会被忽略
常见包括:Accept-Charset、Accept-Encoding、Connection、Content-Length、Cookie / Cookie2、DNT、Host、Origin、Referer、User-Agent 等。
自定义 Header 会使请求变成 “非简单请求”,从而触发请求预检。
OPTIONS 请求#
浏览器发送 OPTIONS 请求: 浏览器先向目标服务器发送一个 OPTIONS 方法的请求
携带自定义 Header 信息:
Access-Control-Request-Headers: Expect Header(如 X-Custom-Header、Authorization 等)。
服务器 Response 中的 Header 指明情况:
- Access-Control-Allow-Headers: 明确允许客户端使用的请求头列表,其中必须包含所有自定义 Header 的名称
- Access-Control-Allow-Origin: 允许的来源
- Access-Control-Allow-Methods: 允许的 HTTP 方法
如果满足要求,浏览器才实际发送带有自定义 Header 的请求,否则阻止请求发送并报错。