Persistent Cookie 实现原理与前端持久化登录实践
谁更灵活?谁更省事?Persistent Cookie 的三种实战方案对比
我最近在重构一个老项目的身份认证模块,后端坚持用 Session + Persistent Cookie 做登录态维持,前端得配合做「自动续期」和「跨页面感知」。本来以为就 setCookie 一行的事,结果踩了一堆坑:Safari 拒绝第三方上下文写入、Chrome 89+ 的 SameSite=Lax 默认行为让登录后跳转直接丢 session、iOS 微信 WebView 里 document.cookie 根本读不到 Secure Cookie……折腾了两天,干脆把几种主流 Persistent Cookie 方案拉出来,真实项目里跑一遍,说说我的血泪体会。
结论先甩这儿:我目前主力用的是「服务端 Set-Cookie + 前端不手动操作 cookie」这一套,配合 fetch 的 credentials: ‘include’ 和响应头里的 Max-Age/Expires 控制生命周期。不是最炫的,但最稳,也最省心。
方案一:纯前端 JS 手动 setCookie(别碰)
就是用 document.cookie = "token=abc123; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=.jztheme.com; secure; HttpOnly=false" 这种方式。我早年很喜欢,因为“全在自己手里”,想啥时候设就啥时候设,想改啥属性就改啥属性。
但真用到生产环境,问题一堆:
- Safari 16.4+ 在无用户交互(比如 onload 里)直接 setCookie,会被静默忽略——你根本不知道它没生效;
- HttpOnly=true 的 cookie,JS 根本读不到,你连“校验是否写成功”都做不到;
- domain 写错一个点(比如写成
domain=jztheme.com而不是domain=.jztheme.com),子域就全挂了; - Secure 属性在本地开发(http://localhost)下直接失效,测试环境和线上行为不一致,排查时怀疑人生。
代码看着简单,实际维护成本高,而且根本不可靠。我现在看到项目里还有这种写法,第一反应就是删掉重写。
方案二:服务端 Set-Cookie + 前端被动接收(我主力用的)
登录接口返回 200,响应头带:
Set-Cookie: auth_token=xyz789; Path=/; Domain=.jztheme.com; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax
前端只管调用登录 API,然后后续所有请求带上 credentials: 'include':
// 登录
fetch('https://jztheme.com/api/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
// 后续请求自动带上 auth_token
fetch('https://jztheme.com/api/profile', {
credentials: 'include'
});
这个方案的优点太实在了:
- HttpOnly 安全性拉满,XSS 基本没法偷 token;
- Max-Age 是服务端控制的,前端不用管过期逻辑,也不用自己算时间戳;
- SameSite 配置由后端统一管理,前端不用操心 CSRF 风险;
- 浏览器原生支持,兼容性从 IE11 到 Chrome 120 全覆盖(只要 header 对得上)。
唯一要注意的坑是:第一次登录后跳转页面,必须确保新页面的请求也带 credentials: ‘include’,否则 cookie 不会发出去。我之前在一个 Vue Router 导航守卫里漏写了,结果用户登录完进首页,发现还是未登录状态,查了半小时才发现是 fetch 没配 credentials。
方案三:localStorage + 定期刷新 Cookie(折中但有隐患)
有些团队为了“前端完全可控”,搞了个混合方案:登录成功后,把 token 存 localStorage,再用这个 token 主动向服务端发个 /refresh 接口,换回一个新的 Persistent Cookie。
// 登录成功后
localStorage.setItem('auth_token', response.token);
// 立即刷新 cookie
fetch('https://jztheme.com/api/refresh-cookie', {
method: 'POST',
headers: { Authorization: Bearer ${response.token} }
});
好处是:token 可以自定义过期策略,比如“30 分钟无操作就清空”,还能配合 localStorage 的事件监听做多标签页同步。
但问题也很明显:
- 多了一次网络请求,首屏体验变差;
- 如果 /refresh 接口失败(网络抖动、服务异常),用户登录成功了却拿不到 cookie,后续请求全 401;
- localStorage 本身可被 XSS 读取,安全性比 HttpOnly 差一截;
- 如果用户手动清空 localStorage,cookie 还在,但前端没记录,就会出现“登出失败”或“状态错乱”。
我在上一家公司用过这个方案,上线后监控发现约 1.3% 的登录请求会出现“已登录但无法访问个人页”的报错,最后定位就是 /refresh 接口超时导致 cookie 没刷上。后来我们砍掉了这个环节,回归纯服务端 Set-Cookie。
我的选型逻辑
看场景,我一般选方案二(服务端 Set-Cookie)。除非遇到两个硬需求:
- 需要精确控制 token 过期时间(比如按分钟级刷新)——那就上方案三,但得加兜底:检查 cookie 是否存在,不存在就重走登录;
- 整个系统是纯静态托管(比如 GitHub Pages 或 S3),没有服务端接口——那只能退回到方案一,但必须加一层检测:
document.cookie.indexOf('auth_token') !== -1,并做好降级提示。
至于方案一?现在除非是 demo 或内部工具,我基本不碰。它看起来自由,实则脆弱,而且越用越累——每次 Safari 更新都要重新测一遍 cookie 行为,不值得。
另外提醒一句:不要迷信“持久化”这个词。Persistent Cookie 本质只是 Max-Age 设得长一点,但用户清浏览器缓存、切换设备、换浏览器,照样失效。真正要做的,是把「登录态」和「用户意图」解耦:登录成功就存凭证,退出就删凭证,别指望 cookie 自己“永远在线”。
以上是我踩坑后的总结,希望对你有帮助。有不同看法欢迎评论区交流——比如你用方案三怎么解决 /refresh 失败的问题?或者你见过哪些更诡异的 cookie 行为?我很好奇。

暂无评论