线上告警:用户上午 10:00 登录成功,10:15 刷新页面却跳回登录页。
排查发现,你给 token Cookie 设置了 Max-Age=900,但运维在 Nginx 又加了一层 proxy_cookie_path,把过期时间改写成 Session。
用户浏览器里同时出现了两条同名 Cookie,一条 15 min 一条 Session,结果请求带的是后者------登录状态瞬间蒸发。
要彻底堵这种坑,得先拆清楚 Cookie 到底由哪些"零件"组成。
解决方案:把 Cookie 拆成 7 个可配置字段
下面这段代码来自我们统一封装的 setCookie 工具,把每个字段都做成可选参数,避免手写字符串拼错。
ts
// 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-Age与Expires二选一,现代浏览器优先Max-Age。SameSite=None必须同时带Secure,否则 Chrome 直接拒绝。
原理剖析:从"键值对"到"安全策略"的三层视角
| 层级 | 字段 | 作用域 | 设计哲学 |
|---|---|---|---|
| 数据层 | name=value |
浏览器 ↔ 服务器 | 纯业务键值,必须 URL 编码 |
| 作用域层 | Domain / Path |
决定 Cookie 随哪些请求头走 | 最小权限原则,防跨域污染 |
| 安全层 | Secure / HttpOnly / SameSite |
决定谁能读、谁能写、何时发 | 纵深防御,层层收紧 |
时序文字图
- 浏览器收到
Set-Cookie: token=abc; Path=/api; Secure; SameSite=Lax - 下次请求匹配
Path=/api且为 HTTPS,才把token=abc塞进Cookie:头。 - 前端 JS 无法读取
HttpOnly,XSS 偷不走;SameSite=Lax挡住 CSRF 的 POST 跨站。
应用扩展:一条 Cookie 的"生命周期"配置片段
生产环境我们拆成三档:
ts
// 会话级:关闭标签即失效
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 允许SecureCookie 通过 HTTP,但 Safari 不行,需加start --ignore-certificate-errors或直接用 HTTPS 自签证书。 - 子域共享登录态时,
Domain=.example.com记得带点,否则只对当前子域生效。
举一反三:三个变体场景实现思路
-
多租户隔离
把
Path设成/tenant/:id,前端切换租户时动态改写 Cookie,避免跨租户串号。 -
A/B 实验灰度
用
Max-Age=3600的短期 Cookie 存分组 ID,服务端按 Cookie 值路由到不同版本,实验结束自然过期,无需清理脚本。 -
第三方埋点兼容
埋点域名
analytics.xxx.com需要回写 Cookie,但主站是www.xxx.com,设置Domain=xxx.com+SameSite=None; Secure,既跨子域又防 CSRF。
小结
- Cookie 不是"一串字符串",而是 7 个独立旋钮的组合。
- 把每个旋钮显式写成参数,就能在代码层面杜绝"拼错分号"这类低级 Bug。
- 真正上线前,用 Chrome DevTools → Application → Cookies 面板再核对一次,比读 RFC 6265 更直观。