HTTP Cookie: 客户端状态的自动存储与使用

2.6K
0
0
最后修改于

本文旨在对当前 HTTP Cookie 的运作机制的进行简要梳理。基于 RFC6265HTTP 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 的有效期和生效范围,分为 AttributeNamePrefix 两种。

具体包括:
有效期控制:

  • 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 在 UA 中通过 (Name, Domain, Path) 的三元组来区分 Cookie。
(session, example.com, /), (session, api.example.com, /) 会被视为不同的 cookie,在请求时可能会带上具有相同 Name 的 Cookie。

生效期,ExpiresMax-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 被关闭后便会被清除。

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 允许重复)

Set-Cookie:无限制,只需要是合法的 urlPath 即可通过校验。

Cookie: 当请求目标 Path 是 Cookie Path 或其子路径,即可携带。

Set-Cookie:无限制。

Cookie:当请求的协议为HTTPS时才允许携带。

Set-Cookie:无限制。

Cookie: 无限制。

当 Cookie 存储在 UA 中时,由浏览器保证带有该属性的 Cookie 不能被 document.cookie 的方式编程访问。只有浏览器在发送 Req 时按需使用。

SameSite#

Set-CookieSecure 属性必须设置,否则忽略。

Cookie: 将当前页面的 Host(地址栏中的 Host)与请求的目标 Host 进行对比。

SameSite 的可选值有三个Strict,Lax,None

  • Strict: 将当前页面的 Host(地址栏中的 Host)与请求的目标 Host 进行同站比对,相同才可以发送。
  • Lax:相比 Strict 更宽松,多一个例外情况(在页面跳转时保持 Cookie):
  1. 当前请求是进行页面导航:浏览器地址栏中显示的 URL 会发生变化。
  2. 且当前请求是安全的HTTP方法: 如 GETHEADOPTIONS
  • None:不进行同站判定。

Lax 是不设置 SameSite 时的默认值,但是相比显式设置的 SameSite=Lax 更为宽松,在Set-Cookie设置的 120s 内,进行 POST 请求也会一并发送 Cookie。

同站判定标准如下:

    1. 协议必须相同,如 httpshttp 不为同站。
    1. eTLD + 1 相同。

eTLD + 1 示例:

  • example.comfe.example.comapi.example.com 互为同站,它们的 eTLD + 1 均为 example.com
  • user1.github.iouser2.github.io 不为同站,因为两者的 eTLDgithub.io,而eTLD + 1 分别为 user1.github.iouser2.github.io

https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies

Set-CookieSecure 属性必须设置,否则忽略。建议与 NamePrefix: __Host-/__Host-HTTP- 一同使用。

对于普通的 Cookie,存储时仅将 (Domain,Name,Path),作为区分。
而分区 Cookie 将设置 Cookie 时访问页面的 Domain 也纳入考虑。
变成了:(ContextDomain, Domain,Name,Path)

Cookie: 在原有基础上,新增 ContextDomain 与当前页面进行比较,只有当当前页面的 Domain 是 ContextDomain 或其子域的情况下才会携带。

NamePrefix#

相比于 Attribute 方案,NamePrefix 虽然有些丑陋,但是却有一些额外的好处。

通过 NamePrefix,将一些 FlagCookie Name 完全绑定在一起,让浏览器本身能确保某个 Cookie Name 一定启用了某些特性,增强 Set-Cookie 的限制。从而在 UA 端阻止一些意料之外的第三方写入(比如 evil.example.com 可以通过 Domain: example.com 写入 Cookie,通过 __Host 保证 evil.example.com 不能写入 example.com)。

  • __Secure: 连接必须为 HTTPS,且其中有设置 Secure

  • __Http: 连接必须为 HTTPS,且其中有设置 SecureHTTPOnly

  • __Host: 浏览器保证 Name 字段不被其他子域设置。(条件:连接必须为 HTTPS,且其中有设置 Secure Attr,Path 为 /,不设置 Domain)

  • __Host - Http: 连接必须为 HTTPS,且其中有设置 SecureHTTPOnly。且浏览器保证 Name 字段不被其他子域设置。

值得注意的是,这些限制从职责分离的角度看用 Attr 似乎更好,但是这需要大改校验逻辑。

比如 __Host:

  • 假设 example.com 写入了一个带有 HostOnly Attr。
  • evil.example.com 试图写入 Domain: example.com(在之前的标准中这个写入请求是合法的)。
  • 因此为了阻止这个写入, 浏览器必须遍历到这条用于 example.com 带有 HostOnly 的 Cookie,发现出现冲突
  • 阻止 evil.example.com 的这次写入。

这种方式无法断绝其他潜在的污染路径,并不合算,而 NamePrefix 这种限制可以完全保证 Name一定启用了某些 Flag,用更为抽象一点的说法是:从 Name + Attr 的语义约束升级为 __Flag-Name 的语法约束,保证其不变性。

当前页面与请求资源的不属于同一个 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 作为 HTTP Header,大小有一定的限制。一般实现为 4KB

  • 每个域名下的 Cookie 数量限制在 50-180 不等

  • 当 Cookie 触及数量 / 容量限制时就需要进行驱逐,通常采用 LRU 策略。

Cookie 使用除 (0-31, 127) 对应的控制字符外的 US - ASCII。一些实现中会对 Cookie 进行 percent-encoding,有助于遵循字符集限制。

作为一个早期客户端的自动化状态机制,Cookie 提供的一定的便捷性,但也为用户带来了一定的安全困扰。其运作机制还存在一些显著不足。如:对于端口不进行区分。

不完善的 Cookie 机制使得在部分场景下没那么好用,比如 SpotifyEmbed,通常作为第三方 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 的请求,否则阻止请求发送并报错。