Cookie 作用域避坑:父域泄漏、同名优先级与多环境隔离

团队里生产、测试、预发常常共用一个顶级域:www.example.comwww.test.example.comwww.uat.example.com。某天测试同学反馈一个诡异现象------明明把界面语言切成了中文,页面却时不时按英文渲染,偶尔还会在两种语言之间来回重定向;而这台机器之前刚访问过生产环境。打开 DevTools 一看,名为 lang 的 cookie 居然有两条:一条 domain 是 .example.com、值是 en,一条 domain 是 .test.example.com、值是 zh

两条同名 cookie、值还不一样,到底谁说了算?这篇文章借这个真实问题,把 Cookie 的作用域(Domain 与 Path)讲透:父域 cookie 为什么会向下泄漏到所有子域、同名 cookie 为什么是「不可区分」的、浏览器和服务端各自会读到哪一个,以及一套可复用的「写入隔离 + 读取自愈 + 正确删除」治理方案。

先厘清:Cookie 的作用域由什么决定

一条 cookie 能不能被某次请求带上,由两个属性决定:DomainPath

Domain 决定哪些主机能收到它,规则有一个很多人没在意的非对称性:

  • 设了 Domain=example.com :这条 cookie 会发给 example.com 以及它的所有子域 ------www.example.comwww.test.example.coma.b.example.com 都带上。
  • 没设 Domain(host-only) :只发给写它的那个精确主机,子域都收不到。
  • 子域写的 cookie 不会向上 泄漏到父域,也不会横向泄漏到兄弟子域。
写入时的 Domain 哪些主机的请求会带上
example.com example.com 及全部子域(含 test / uat 子域)
test.example.com test.example.com 及其子域
不设(host-only) 仅写入它的那个精确主机

换句话说,父域 cookie 单向向下覆盖整棵子域树

flowchart TD P[父域 example.com 写入的 cookie] --> A[www.example.com 收到] P --> B[www.test.example.com 收到] P --> C[www.uat.example.com 收到] S[子域 test.example.com 写入的 cookie] --> B

父域那条对全部子域可见(一对多),子域那条只有它自己能收到(一对一)------单向、不可逆。这点是后面所有问题的根。

Path 决定哪些路径能收到它 :默认是写入时所在的目录,只有 path 前缀匹配的请求才带。日常大多写 path=/,所以 Path 引发的问题比 Domain 少,但原理一样------它也是 cookie「身份」的一部分。

一次请求会把所有 匹配(domain + path)的 cookie 拼成一个 Cookie 请求头发出去。问题就出在「所有匹配的都带上」这一句。

这是整篇文章的核心。Cookie 请求头里只有 name=value,不携带 domain、path、Secure 等任何属性。RFC 6265 对此有明确规定:服务端无法从 Cookie 头判断一条 cookie 的过期时间、对哪些主机/路径有效、是否带了 Secure 或 HttpOnly。

于是当浏览器里存在两条同名 lang(一条来自 .example.com、一条来自 .test.example.com),服务端收到的就是这样一串:

ini 复制代码
Cookie: lang=zh; lang=en

两个 lang没有任何信息能告诉你谁来自哪个 domain。那「谁排在前面」总有规律吧?RFC 6265 确实规定了用户代理(浏览器)的排序方式:

Cookies with longer paths are listed before cookies with shorter paths. Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.

即「path 长的在前,path 等长时创建时间早的在前」。但同一段紧接着给出了最关键的告诫:

if the Cookie header contains two cookies with the same name (e.g., that were set with different Path or Domain attributes), servers SHOULD NOT rely upon the order in which these cookies appear in the header.

也就是说:两条同名 cookie 出现时,服务端绝对不应依赖它们在头里的顺序 。而且规范还附了一句 NOTE:并非所有浏览器都按这个顺序排。叠加 path 等长(都是 /)、创建时间又取决于用户先访问了哪个环境------你最终「读到的第一个」是跨浏览器、跨时机的非确定结果

客户端同样躲不掉。document.cookie 也只是一串 name=value,无论是手写正则还是用 js-cookie,拿到的都是「碰到的第一个」:

ts 复制代码
// 两条同名 lang 都在时,下面两种写法都只会拿到其中一个,且哪一个不确定
const v1 = document.cookie.match(/(?:^|;\s*)lang=([^;]+)/)?.[1];
const v2 = Cookies.get('lang'); // js-cookie 内部同样是解析 document.cookie 取第一个

核心论断:同名 cookie 一旦在不同 domain/path 上同时存在,「读哪一个」就成了不可控的非确定行为------浏览器、服务端、各种库都救不了你。

为什么多环境共用顶级域最容易中招

把上面两点合起来,就能解释开头那个现象。假设语言偏好存在 cookie lang 里:

  1. 用户先访问生产 www.example.com,切了英文,应用把 lang=en 写到 Domain=example.com(为了让所有子产品共享)。这条 cookie 因此对全部 *.example.com 可见。
  2. 用户又访问测试 www.test.example.com,切了中文,测试环境把 lang=zh 写到自己的 Domain=test.example.com
  3. 此刻在测试子域发请求,浏览器把两条都带上 :来自父域的 en 和来自本环境的 zh
flowchart TD U[在 test 子域发起请求] --> H[Cookie 头出现两条同名 lang] H --> L1[一条来自 example.com 值为 en] H --> L2[一条来自 test.example.com 值为 zh] L1 --> R[服务端只看到 name 与 value 不含 domain] L2 --> R R --> X[无法区分来源 且顺序不可依赖] X --> Y[读到的值非确定 行为飘忽]

注意这个污染是单向的:生产写的父域 cookie 会泄漏到 test / uat 子域,但 test 写的子域 cookie 不会反向影响生产。所以现象往往表现为「在生产改了设置,测试/预发跟着遭殃」,而生产自己始终正常------这也是它特别难复现、特别容易被当成偶发的原因。

语言只是个例子。任何「服务端或客户端按 cookie 取值分支」的逻辑,都会被这条飘忽的值带偏:

  • 服务端中间件按 cookie 决定路由 / 重定向 (比如按语言给 URL 加 /en 前缀):这次读到 en 重定向到英文路径,下次读到 zh 又跳回来,最坏情况是重定向回环
  • 由 cookie 派生的请求头 (如 Content-Language 或自定义语言头):后端按错误的语言返回内容,于是出现「切了语言但接口数据/文案不变」的不一致。
  • 客户端条件渲染、SDK 初始化:组件按 cookie 选语言、选地区、选灰度分支,渲染结果在刷新之间跳来跳去。

这类 bug 有很统一的特征:间歇性、与「最近访问过哪些环境」强相关、手动清掉 cookie 就好一阵子。一旦看到这三点同时出现,优先怀疑「同名 cookie 状态污染」。

怎么确认你中了这招

排查只要一步------确认是否存在同名多条 cookie:

  • DevTools → Application → Cookies :重点看 Domain 这一列,是否有两行同名、domain 不同。这是唯一能直观看到 domain 来源的地方。
  • 控制台快速计数
js 复制代码
document.cookie.split('; ').filter((c) => c.startsWith('lang=')).length
// > 1 就说明有重复

这里有个认知差要记住:DevTools 能按 domain 区分,但你的代码(document.cookie、请求头)永远看不到 domain。排查视角和运行时视角的这点不对称,正是这类问题反直觉的根源。

解决方案:写入隔离 + 读取自愈 + 正确删除

按「治本 → 兜底 → 收尾」分三层。

最根本的修法是从源头杜绝同名重复------每个环境用各自精确的 domain 写 :测试写 test.example.com、生产写 example.com,互不可见,自然不会重复。

判断原则很简单:

  • 这份状态需要跨子域共享吗?登录态可能需要(多个子产品同一个账号),那才写父域。
  • 语言、灰度、AB 分流、主题这类「每个环境本该独立」的状态,写精确子域,不要图省事写到顶级父域。

但现实里你常常改不动 那个往父域写 cookie 的系统------它可能是后端的 Set-Cookie、是另一个历史应用、是你无权改的网关。所以光有治本还不够。

兜底:读取前做一次「孤儿 cookie」自愈清理

思路是:检测到同名 cookie 出现 2 条及以上时,主动把「泄漏来的父域那条」删掉,只留本环境的。一个脱敏的通用实现:

ts 复制代码
// 仅对已知会被父域污染的子域层级,返回需要清理的泄漏父域;其它一律不动
function getLeakedParentDomain(hostname: string): string | null {
  if (hostname.endsWith('.test.example.com') || hostname.endsWith('.uat.example.com')) {
    return 'example.com';
  }
  return null;
}

let cleaned = false;
function cleanDuplicateCookie(name: string): void {
  if (cleaned || typeof document === 'undefined') return;
  cleaned = true; // 一次性,避免每次读取都扫一遍

  const count = document.cookie.split('; ').filter((c) => c.startsWith(name + '=')).length;
  if (count <= 1) return; // 只有一条(含生产环境的正常情况)就不处理

  const parent = getLeakedParentDomain(location.hostname);
  if (!parent) return;

  // 删除必须用与写入时完全一致的 domain/path 才能命中
  document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${parent}`;
}

几个设计要点:

  • 保守触发:只有检测到 2 条以上才动手,生产环境永远只有 1 条、绝不会被误删。
  • 只清已知层级getLeakedParentDomain 白名单式地只处理已知会被污染的子域,未知环境直接返回 null 不动,避免误伤。
  • 调用时机:放在应用启动早期、或每次读取该 cookie 之前调一次(用一次性 flag 防重复)。清掉父域那条后,浏览器后续请求只带本环境的一条,服务端和客户端读取就都恢复确定了。

「删 cookie」是这类问题里最容易翻车的一步,因为 JS 根本没有删除 API,只能用「空值 + 过去的过期时间」覆盖,让浏览器立刻丢弃它。围绕这点有三条必须记住:

  • domain / path 必须与写入时完全一致 才能命中。否则你只是新增了一条 scope 不同的同名过期 cookie,原 cookie 纹丝不动------「删了个寂寞」。来源不确定时,对几种常见组合(host-only、父域 .example.com)各写一遍覆盖删除即可;不匹配的尝试是无操作,安全。
  • 删除指令受当前安全上下文约束 :HTTP 页面下带 SecureSet-Cookie 会被忽略。用 js-cookie 这类库时,写入和删除要用同一套 domain/path/secure/sameSite,否则照样删不掉。
  • 子域能删父域 cookie 吗? 可以。从 test.example.comdomain=example.com(可注册的父域)是被允许的;但你不能写 domain=com(公共后缀,在 Public Suffix List 上会被浏览器拒绝),也不能写别人的域。

还有一条局限 要诚实说明:客户端自愈对用户的首个请求无效 ------cookie 已经在入站请求里了,要等清理脚本跑完、下一个请求才干净。要在首个响应就修好,得在服务端用响应的 Set-Cookie 把泄漏的父域 cookie 置过期,或在中间件里处理。

一份可复用的 checklist

  • 写 cookie 必须显式、稳定 地指定 domain + path;同一条 cookie 在它的整个生命周期里用同一套属性(写入、更新、删除都一致)。
  • 「每个环境本该独立」的状态(语言 / 灰度 / AB / 主题)→ 写精确子域,不要写顶级父域。
  • 多环境共用顶级域时,默认假设父域 cookie 会向下泄漏到所有子域;对关键 cookie 加「同名去重自愈」。
  • 读取同名敏感 cookie 前,先确认没有重复;服务端永远不要依赖 Cookie 头里的顺序(RFC 6265 明确规定)。
  • 删 cookie:空值 + 过去时间 + 完全匹配 domain/path/secure/sameSite;来源不明就多组合覆盖删。
  • 别只靠客户端自愈------它对首个请求 无效;要彻底,就在服务端响应 / 中间件里用 Set-Cookie 置过期。

小结

Cookie 的麻烦几乎都来自同一个根源:它的 domain/path 是写入时的隐藏状态,读取时却完全看不见 。浏览器按 domain 区分着存,你的代码却只能拿到 name=value;一旦同名 cookie 在不同 scope 上共存,「读哪个」就交给了运气。把 domain/path 当成 cookie 身份的一部分来显式管理------写入隔离、读取去重、删除匹配------多环境共域那些诡异的、间歇的、复现不出来的语言/重定向问题,就消了大半。

参考与数据源

相关推荐
api工厂1 小时前
ZCode 3.0 版本搭配GLM-5.2能力测试
前端·人工智能·ai
小小小小宇1 小时前
单点登录(二)
前端
阿猫的故乡2 小时前
Vue + Axios 从入门到封装:拦截器、错误处理、请求取消、接口管理全搞定
前端·javascript·vue.js
良逍Ai出海2 小时前
免费模板搭完独立站后,我用 Codex + Figma 做了自己的页面设计
前端·人工智能·figma
纽格立科技2 小时前
DRM 发射端链路图(下)
前端·人工智能·车载系统·信息与通信·传媒
代码小库2 小时前
【2026前端转 AI 全栈指南】第 2 章(下):NestJS 项目创建 · MongoDB 配置 · 项目启动与调试
前端·数据库·mongodb
之歆2 小时前
Promise 基础技术深度解析:从回调地狱到链式调用
前端·okhttp·promise
甲维斯2 小时前
国产版“Codex”初体验,智谱ZCode很强啊!
前端·人工智能·ai编程
道友可好2 小时前
AI 怎么自己跑完一个 6 小时的任务?
前端·人工智能·后端