一次诡异的登录失效

333 阅读3分钟

线上告警:用户上午 10:00 登录成功,10:15 刷新页面却跳回登录页。
排查发现,你给 token Cookie 设置了 Max-Age=900,但运维在 Nginx 又加了一层 proxy_cookie_path,把过期时间改写成 Session
用户浏览器里同时出现了两条同名 Cookie,一条 15 min 一条 Session,结果请求带的是后者——登录状态瞬间蒸发。
要彻底堵这种坑,得先拆清楚 Cookie 到底由哪些“零件”组成。


解决方案:把 Cookie 拆成 7 个可配置字段

下面这段代码来自我们统一封装的 setCookie 工具,把每个字段都做成可选参数,避免手写字符串拼错。

// utils/cookie.ts
export const setCookie = (opts: {
  name: string;
  value: string;
  maxAge?: number;      // 秒
  expires?: Date;
  path?: string;
  domain?: string;
  secure?: boolean;
  sameSite?: 'Strict' | 'Lax' | 'None';
  httpOnly?: boolean;   // 🔥 仅服务端可写
}) => {
  const parts = [
    `${opts.name}=${encodeURIComponent(opts.value)}`,
    opts.maxAge   && `Max-Age=${opts.maxAge}`,
    opts.expires  && `Expires=${opts.expires.toUTCString()}`,
    opts.path     && `Path=${opts.path}`,
    opts.domain   && `Domain=${opts.domain}`,
    opts.secure   && 'Secure',
    opts.sameSite && `SameSite=${opts.sameSite}`,
    opts.httpOnly && 'HttpOnly',
  ].filter(Boolean);
  document.cookie = parts.join('; ');
};

🔍 关键决策点

  • encodeURIComponent 兜住中文或特殊符号。
  • Max-AgeExpires 二选一,现代浏览器优先 Max-Age
  • SameSite=None 必须同时带 Secure,否则 Chrome 直接拒绝。

原理剖析:从“键值对”到“安全策略”的三层视角

层级字段作用域设计哲学
数据层name=value浏览器 ↔ 服务器纯业务键值,必须 URL 编码
作用域层Domain / Path决定 Cookie 随哪些请求头走最小权限原则,防跨域污染
安全层Secure / HttpOnly / SameSite决定谁能读、谁能写、何时发纵深防御,层层收紧

时序文字图

  1. 浏览器收到 Set-Cookie: token=abc; Path=/api; Secure; SameSite=Lax
  2. 下次请求匹配 Path=/api 且为 HTTPS,才把 token=abc 塞进 Cookie: 头。
  3. 前端 JS 无法读取 HttpOnly,XSS 偷不走;SameSite=Lax 挡住 CSRF 的 POST 跨站。

应用扩展:一条 Cookie 的“生命周期”配置片段

生产环境我们拆成三档:

// 会话级:关闭标签即失效
setCookie({ name: 'ui-theme', value: 'dark', path: '/', sameSite: 'Lax' });

// 持久化:7 天免登录
setCookie({
  name: 'refreshToken',
  value: jwt,
  maxAge: 7 * 24 * 3600,
  path: '/auth',
  secure: true,
  sameSite: 'Strict',
});

// 服务端专用:前端不可见
setCookie({
  name: 'sessionId',
  value: uuid,
  httpOnly: true,
  secure: true,
  sameSite: 'None',
});

环境适配说明

  • 本地 localhost 开发时,Chrome 允许 Secure Cookie 通过 HTTP,但 Safari 不行,需加 start --ignore-certificate-errors 或直接用 HTTPS 自签证书。
  • 子域共享登录态时,Domain=.example.com 记得带点,否则只对当前子域生效。

举一反三:三个变体场景实现思路

  1. 多租户隔离
    Path 设成 /tenant/:id,前端切换租户时动态改写 Cookie,避免跨租户串号。

  2. A/B 实验灰度
    Max-Age=3600 的短期 Cookie 存分组 ID,服务端按 Cookie 值路由到不同版本,实验结束自然过期,无需清理脚本。

  3. 第三方埋点兼容
    埋点域名 analytics.xxx.com 需要回写 Cookie,但主站是 www.xxx.com,设置 Domain=xxx.com + SameSite=None; Secure,既跨子域又防 CSRF。


小结

  • Cookie 不是“一串字符串”,而是 7 个独立旋钮的组合。
  • 把每个旋钮显式写成参数,就能在代码层面杜绝“拼错分号”这类低级 Bug。
  • 真正上线前,用 Chrome DevTools → Application → Cookies 面板再核对一次,比读 RFC 6265 更直观。