Cookie、Session、LocalStorage 和 SessionStorage 这四个词大概是每个做 Web 的人都绕不过去的一道坎。我见过不少工作两三年的朋友能把它们的区别背得滚瓜烂熟可真到了线上出问题的时候——登录态莫名其妙丢失、新开一个标签页数据不见了、用户上传大图之后页面卡成幻灯片——却完全不知道从哪里下手。原因很简单这四样东西的差别从来不是一张对照表能说清的它们各自站在不同的层、解决不同的问题、也各自带着不同的坑。这篇内容我想把这四个东西从背答案拉到能拍板的层面。前半部分讲清楚每个机制到底处在架构的哪一层、为什么当初要设计成这个样子中间拆容量、生命周期、作用域和安全属性把参数背后的账算给你看后半部分是一套可以直接抄的登录态落地方案、前端存储的封装写法以及我在实际项目里踩过的、排查过的那些问题。不管你是刚入行的新手还是已经写过不少业务代码、但对底层还是一知半解的老手看完至少能做到拿到一个需求三秒钟判断该把数据放在哪里以及为什么不能放在另一个地方。1. 四种存储到底各自站在哪一层1.1 先把HTTP 是无状态这件事说明白要理解这四样东西为什么同时存在得先回到那个最原始的问题HTTP 协议本身不记事。浏览器发一个请求服务端处理完返回结果然后双方就互相遗忘了。你打开一个页面点下一个链接对服务端来说这就是一个全新的、素不相识的访客。这个设计在早期其实是优点——简单、可扩展、服务器不需要为每个访客保留上下文。但一旦网站需要记住某个人比如他刚才登录过、购物车里放了两件商品、界面语言选了中文麻烦就来了。所有的存储机制本质上都是在给这个无状态的协议人为地接上一根记忆的管子。Cookie 是把记忆塞进每次请求的头部让浏览器自动带上Session 是把记忆放在服务端只给客户端一个编号LocalStorage 和 SessionStorage 则是干脆把记忆留在浏览器本地压根不参与和服务端的对话。理解了这条主线后面所有的差异就都顺理成章了。为什么 Cookie 只有 4KB因为它要跟着每一个请求飞一趟太大就是灾难。为什么 Session 需要服务端存储因为它压根就不在客户端。为什么 LocalStorage 不能存登录凭证因为它纯粹是前端的东西没有任何服务端能对它做控制。我经常用一个类比跟新人解释Cookie 像是你每次进小区都要刷的门禁卡卡里存的信息很少但你得随身带着进门就得刷Session 像是物业前台的登记本门禁卡上只有一个编号保安拿着编号去本子上查你是谁LocalStorage 像是你自己家里的抽屉东西放在里面出门不会自动带上但只要不搬家就一直都在SessionStorage 则是你这次出门随身带的包回家关掉标签页就清空了。1.2 Cookie浏览器里唯一能自动跟着请求跑的东西Cookie 的核心特征只有一个但它是决定性的浏览器会自动把匹配的 Cookie 附加到同域请求的头部。这一条让它成为唯一能天然承载服务端会话标识的客户端存储方案也是它至今无法被完全替代的原因。具体流程是这样的服务端在响应头里写Set-Cookie: sidabc123; Path/; HttpOnly浏览器收到后把它存起来。之后每一次向同域发起的请求浏览器都会自动加上Cookie: sidabc123这一行。整个过程前端代码一行都不用写也拦不住——这既是它的便利也是它的风险来源。Cookie 的匹配规则由Domain和Path两个属性控制很多人对这块是模糊的。Path/admin意味着只有请求/admin以及它下面的路径时才会带上/admin/user会带但/administrator不会——它是按路径段匹配的不是按字符串前缀。Domain不设置时默认是当前域名且不含子域一旦显式设置成.example.com这种形式所有子域就都能共享。还有个特别容易被忽略的点document.cookie的写入是追加而不是覆盖。你写document.cookie a1再写document.cookie b2读出来是a1; b2。只有同名的、同 Domain 同 Path 的 Cookie 才会互相覆盖。我见过有人以为每次赋值会清空结果线上攒了几十个同名不同 Path 的脏 Cookie请求头越滚越大。1.3 Session一个存在服务端的档案柜Cookie 只是取件码Session 经常被和 Cookie 并列着讲其实它俩根本不是一个层面的东西。Session 是服务端的一种数据存储机制Cookie 只是它在客户端的那张取件码。服务端收到登录请求、校验密码通过之后会在自己的存储里开一个记录比如sess_9f8a7b: { userId: 1024, role: admin, loginAt: ... }然后把sess_9f8a7b这个随机串通过Set-Cookie发给浏览器。此后每次请求服务端拿着 Cookie 里的这个串去自己的存储里查查到就认人。这个存储具体是什么是 Session 方案里最需要做决策的地方。单机小项目直接用进程内存各种框架默认的 MemoryStore就够了重启即丢简单粗暴。但只要上了多实例部署内存方案立刻崩盘——用户第一次请求打到 A 机器Session 存在 A 的内存里第二次请求被负载均衡甩到 B 机器B 不认识这个 SessionID用户就被踢下线了。所以生产环境一般会把 Session 放到一个所有实例都能访问的共享存储里同时给 Session 设置合理的过期时间。这也解释了为什么现在很多项目干脆不用服务端 Session 了无状态的令牌方案比如把用户信息签名后塞进一个自包含的字符串里不需要服务端存任何东西横向扩容时天然没有粘滞问题。但 Session 的优势也很明确——服务端随时能销毁它。用户点了退出所有设备或者管理员要封禁某个账号服务端删掉那条记录令牌立刻失效。自包含令牌要做到这一点就得额外维护一张黑名单等于又把状态加回来了。1.4 LocalStorage 与 SessionStorage纯前端的本地仓库Web Storage 这两个兄弟和前两者最大的不同在于它们完全不参与网络通信。存进去的东西不会自动发给服务端服务端也看不见读写全靠 JavaScript 手动操作。两者的 API 完全一样就setItem、getItem、removeItem、clear这几个方法加上一个length属性和key(index)方法。值只能存字符串存对象必须先JSON.stringify取出来再JSON.parse。这一点新手经常忘直接存对象进去读出来是[object Object]然后在代码里对着这个字符串做属性访问报错报得莫名其妙。它们之间唯一的区别就是生命周期和作用域LocalStorage 是同源共享、永久保存除非用户手动清、代码主动删或者浏览器因为存储压力把它回收掉SessionStorage 是每个标签页独立一份关掉标签页就没了同一个页面里刷新不会丢但新开一个标签页就是全新的空仓库。举个具体场景帮你建立直觉用户的界面主题选了深色这个偏好应该放 LocalStorage因为他下次来还想看到深色用户正在填一个五步注册表单填到第三步手滑刷新了页面这个草稿应该放 SessionStorage因为这次流程结束之后它就毫无价值了没必要长期占着地方。2. 差异逐项对照容量、生命周期、作用域、通信成本2.1 容量上限4KB 和 5MB 差了一千多倍容量这件事不能只记数字得理解数字背后的约束逻辑。Cookie 的单条上限大约是 4KB4096 字节包含名字、值和各种属性。这个限制不是随便定的是因为 Cookie 要跟着每一个请求走如果允许存几百 KB那一个页面上几十个静态资源请求全部带上网络直接瘫痪。除了单条限制每个域名下的 Cookie 总数也有上限主流浏览器大概在 50 个上下超出之后浏览器会按某种策略淘汰旧的。所以 Cookie 能表达的信息量极其有限它天生只适合做标识不适合做数据。LocalStorage 和 SessionStorage 的配额是每个源 5MB 左右部分浏览器给了 10MB。注意是每个源——https://a.example.com和https://b.example.com是两个不同的源各自独立计算 5MB。这个量级存点用户偏好、缓存一些列表数据完全够用但如果你想把一整套离线数据或者几十兆的文件塞进去那方向就错了那是 IndexedDB 的地盘。Session 的容量取决于服务端存储介质理论上没有硬上限但你得考虑成本。一个用户存 100KB 的 Session 数据十万在线用户就是 10GB 内存这个账必须在设计阶段就盘清楚。我的习惯是 Session 里只放 userId、角色、少量权限标记加起来控制在 1KB 以内其他数据按需查库或者放缓存。特性CookieSessionLocalStorageSessionStorage存储位置浏览器服务端浏览器浏览器容量上限约 4KB/条取决于服务端存储约 5MB/源约 5MB/源默认生命周期会话级可设过期服务端配置永久除非主动删标签页关闭即清是否自动随请求发送是否靠 Cookie 带 ID否否服务端可读是是否否JS 可读受 HttpOnly 限制否是是跨标签页共享是是是同源否2.2 生命周期谁在什么时候消失生命周期这块四个机制的处理方式完全不同搞混了会出很诡异的问题。Cookie 分两种。不设置Expires或Max-Age的是会话 Cookie存在内存里浏览器关掉就没了设置了过期时间的会写到磁盘上到期才清。这里有个坑现在很多浏览器有恢复上次会话的功能关掉再打开会话 Cookie 有可能还在所以不能把关浏览器就一定清当成安全前提。真要清服务端主动下发一个Max-Age0的过期 Cookie 覆盖掉才靠谱。Session 的过期是由服务端控制的通常有两种策略滑动过期每次访问都刷新过期时间用户一直活跃就一直有效和绝对过期从创建那一刻起算到点就废不管用户多活跃。我一般两个都设滑动 30 分钟绝对 7 天。前者防止用户离开工位没锁屏被人接着用后者强制用户定期重新认证也是合规审计常见的要求。LocalStorage 理论上永久但实际有三个变数用户手动清理浏览器数据、浏览器的存储压力回收机制、无痕/隐私模式下关闭窗口即清空。最后这条经常被忽略——你在无痕窗口里调试一切正常用户开了无痕就发现记住我失效了投诉过来你还一头雾水。所以凡是依赖 LocalStorage 的功能代码里都必须有读不到就走默认值的兜底分支。SessionStorage 的生命周期跟着标签页走刷新不丢关闭即清。但这里有个细节值得单独说通过window.open打开的同源新窗口会复制一份源窗口的 SessionStorage 作为初始值。复制完成之后两边就是各自独立的副本了互不影响。而通过链接点击、地址栏输入打开的新标签页SessionStorage 是空的。这个行为不一致做多窗口协作功能的时候非常容易踩。2.3 作用域同源、域名、路径、标签页的四种切法作用域的差异经常是数据莫名其妙不见了或数据串了的根源。Cookie 的作用域由 Domain 和 Path 共同决定是四个机制里最灵活的。Domain 控制到哪个域名层级共享Path 控制到哪个路径前缀生效。这套机制在早年多应用共用一套登录态的时代很好用比如把登录 Cookie 的 Path 设成/各个子系统就都能读到。但它也带来一个隐患同域下的任何一个子系统被攻破都能读到这份 Cookie边界其实很脆弱。LocalStorage 的作用域是严格的同源策略——协议、域名、端口三者完全一致才共享。http://example.com和https://example.com是两个不同的源数据不互通example.com和www.example.com也是两个不同的源。这个严格性有时候会带来迁移的麻烦站点从 HTTP 切到 HTTPS用户的登录态和偏好设置全部读不到了得做一次平滑的迁移处理。SessionStorage 在 LocalStorage 的基础上又加了一层标签页隔离。同一个源标签页 A 和标签页 B 的 SessionStorage 互不可见。这既是优点多标签同时填不同的表单互不干扰也是缺点你想在多个标签页之间同步一个临时状态就做不到了。Session 的作用域取决于 SessionID 存在哪里。如果 SessionID 放在 Cookie 里那它的作用域就跟 Cookie 一致如果放在 URL 参数里不推荐那作用域会跟着链接走特别容易泄露如果放在自定义请求头里那就完全由前端代码控制发送时机了。2.4 自动携带这件事把性能账算给你看Cookie 最容易被低估的成本就是自动携带。这件事在小站点上无感一旦流量上来账就很吓人。算一笔假设你在 Cookie 里放了 2KB 的数据比如一些用户信息、追踪标记站点每天有 100 万次请求包括页面、接口、静态资源如果静态资源和页面同域的话。那么每天因为 Cookie 额外产生的上行流量是 2KB × 100 万 2GB。一个月就是 60GB 的纯浪费而且这些流量完全没有任何业务价值。更隐蔽的影响在首屏。移动网络下的上行带宽本来就紧张每个请求都要多背几 KB请求数一多整体感知延迟就上去了。这也是为什么现在的最佳实践是把静态资源放到独立域名或者 CDN 上——不光是并发连接数的考虑也是为了让那些域名下压根没有 Cookie请求干干净净。所以我的原则是Cookie 里只放必须让服务端看到的、且体积极小的东西。会话标识、少量的灰度标记、CSRF 令牌这些可以。用户昵称、头像地址、权限列表这种一律不要往 Cookie 里塞用一次接口查一次或者放在前端的本地存储里需要的时候通过请求头或请求体带过去。3. 安全视角XSS、CSRF 与三个必须配的属性3.1 XSS 能读到什么决定了你不能把什么放在哪里XSS跨站脚本注入的本质是攻击者的代码跑在了你的页面上于是它拥有了你的页面所拥有的一切权限——包括读document.cookie、读 LocalStorage、读 SessionStorage、代替用户发请求。这就引出了一个非常关键但经常被搞反的结论LocalStorage 在 XSS 面前是毫无防御能力的。只要页面存在 XSS 漏洞攻击者一行localStorage.getItem(token)就能把你辛苦存的令牌拿走然后在他自己的机器上用这个令牌冒充用户。而且 LocalStorage 里的东西没有过期概念令牌一旦泄露只要服务端不主动作废攻击者可以一直用。Cookie 则可以靠HttpOnly属性把 JS 挡在门外。设置了HttpOnly的 Cookiedocument.cookie读不到XSS 注入的脚本也就拿不走。这是 Cookie 至今仍然是登录态首选载体的核心原因——不是因为它方便而是因为它是唯一一个能对 JS 隐藏的客户端存储。我说的登录态不要放 LocalStorage说的就是这件事。很多人喜欢把 JWT 存在 LocalStorage 里图的是简单、跨域方便、前端能读出来判断过期。但代价是把防线的最后一道交给了页面绝对不能有 XSS这个假设而这个假设在真实项目里几乎不可能成立——第三方脚本、富文本渲染、老旧的依赖库任何一个环节出问题都可能导致注入。3.2 CSRF 为什么能得逞SameSite 又解决了什么CSRF跨站请求伪造的逻辑完全反过来攻击者不需要读你的任何数据他只需要让你在不知情的情况下发出一个请求。比如在别的站点的页面里放一个指向你银行转账接口的隐藏表单用户访问那个页面浏览器会自动带上你在银行的 Cookie请求就成功了。这里的关键在于自动携带这个特性被武器化了。Cookie 会自动跟着请求走浏览器不会问用户这个请求是你本意吗。SameSite属性就是专门治这个的。它有三个值Strict最严格任何跨站请求都不带这个 Cookie。从外部链接点进来第一次打开的页面就是未登录状态体验会有点怪。Lax折中方案跨站的顶层导航比如点击链接跳转过来的 GET 请求会带跨站的子资源请求、POST 请求不带。这是目前主流浏览器的默认值也是绝大多数场景下最合适的配置。None完全不限制跨站也带。但必须同时设置Secure也就是只能走 HTTPS。这个值主要是给真正需要跨站携带的场景用的比如嵌入到别人页面里的组件。除了 SameSite还有一个经典的双重保险CSRF Token。服务端在页面渲染时下发一个随机串前端在发写操作请求时把它放在请求头或请求体里。攻击者的跨站请求构造不出这个值请求就废了。这个方案和 SameSite 是互补的SameSite 依赖浏览器实现CSRF Token 依赖服务端校验双管齐下基本能覆盖绝大多数场景。3.3 HttpOnly、Secure、SameSite 三件套的推荐配置我把常用的配置组合整理成了一张表直接照着配基本不会出错场景HttpOnlySecureSameSite说明登录会话标识是是Lax最关键的一条JS 必须读不到CSRF Token存 Cookie 时否是Lax前端需要读出来放进请求头用户偏好语言、主题否视情况Lax不敏感但也不该被跨站滥用灰度标记、实验分组否视情况Lax体积要控制别塞太多第三方嵌入组件用视情况是None必须配 Secure慎用这里单独说一个容易搞错的地方很多人以为给 Cookie 设了Secure就安全了其实Secure只保证这个 Cookie 不会通过明文 HTTP 发送。如果你的站点本身就允许 HTTP 访问攻击者可以做降级攻击先把用户打到 HTTP 上Cookie 就裸奔了。所以真正的做法是整站强制 HTTPS用 HSTS 让浏览器记住这个策略Secure只是在最后再加一道锁。还有__Host-和__Secure-这两个前缀虽然不常用但值得知道。给 Cookie 名字加上__Host-前缀浏览器会强制要求这个 Cookie 必须满足三个条件设置Secure、设置Path/、且不能设置Domain。这相当于用浏览器的规则帮你锁死了作用域防止被子域覆盖。在安全要求高的场景里登录 Cookie 用这个前缀是个好习惯。3.4 Session 的固定攻击与登录后的会话轮换用 Session 的时候有个细节很多自研的会话管理都漏掉了用户登录成功后必须重新生成一个 SessionID。原因是这样攻击者可以先自己访问一次站点拿到一个合法的 SessionID此时是未登录状态然后想办法让受害者的浏览器也用上这个 ID——比如通过一个带有该 ID 的链接引诱受害者点击。受害者点进来之后完成登录服务端就把这个 SessionID 标记成已登录的某某用户。而攻击者手里还握着同一个 ID他一刷新直接就进到受害者的账号里了。这个攻击手法叫会话固定防御方式很简单登录成功的那一刻销毁旧的 Session 记录生成一个全新的 SessionID 下发。同样权限变更比如普通用户提权成管理员、修改密码之后也应该做一次轮换。另一个容易被忽视的点是退出登录。很多实现只是前端把 Cookie 删掉了服务端的 Session 记录还留着直到自然过期。如果这个 SessionID 之前通过某种方式泄露过比如日志里打印了、或者用户共享了链接攻击者仍然能用它。所以退出登录必须双管齐下服务端删记录同时下发一个过期 Cookie 覆盖客户端。4. 落地实操一套能直接抄的登录态与存储封装4.1 服务端下发 Cookie 的关键参数怎么写先看服务端这一侧。下面这段是通用的思路用任意服务端语言写都差不多关键在于那几个属性一个都不能少HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: __Host-sid8f3a9c2e7b1d; Path/; Max-Age1800; HttpOnly; Secure; SameSiteLax Set-Cookie: csrf_tokenk29d8f1a; Path/; Max-Age1800; Secure; SameSiteLax几个参数的取值理由我逐个说一下Max-Age1800就是 30 分钟和前面说的滑动过期策略对齐。注意Max-Age的优先级高于Expires两个都写的时候浏览器认Max-Age。用Max-Age的好处是不用处理时区和格式问题Expires要求是 GMT 格式的日期字符串手写极容易出错。Path/而不是Path/api是因为很多站点既有接口请求也有页面请求需要读这个 Cookie。如果你的前端和服务端完全分离、部署在不同域名下那就得考虑跨域携带的问题了这时候 SameSite 要设成None且必须配Secure同时服务端要正确响应 CORS 的凭证头。这套配置比较绕能不跨域尽量别跨。Secure和HttpOnly前面解释过了一个防明文传输一个防脚本读取。这两个在开发环境经常要临时关掉本地是 HTTPSecure会让 Cookie 压根不生效我的建议是用配置项控制别在代码里写死。上线前用检查清单过一遍确认生产环境的配置里这两个开关都是开的。4.2 前端存储的一个通用封装原生 API 太裸了直接到处调localStorage.setItem会带来三个问题没有命名空间容易冲突、存对象要手动序列化容易忘、没有过期概念用完就永远是垃圾。封装一层能解决大部分麻烦下面这版我在几个项目里都用过可以直接拿去改const PREFIX app_v1_; const store { set(key, value, ttlMs) { const payload { v: value, e: ttlMs ? Date.now() ttlMs : null }; try { localStorage.setItem(PREFIX key, JSON.stringify(payload)); return true; } catch (err) { // 配额超限先尝试清理本应用的过期项 store.cleanExpired(); try { localStorage.setItem(PREFIX key, JSON.stringify(payload)); return true; } catch (e) { return false; } } }, get(key, fallback null) { const raw localStorage.getItem(PREFIX key); if (!raw) return fallback; try { const payload JSON.parse(raw); if (payload.e Date.now() payload.e) { localStorage.removeItem(PREFIX key); return fallback; } return payload.v; } catch (e) { // 数据被污染直接删掉 localStorage.removeItem(PREFIX key); return fallback; } }, cleanExpired() { const now Date.now(); const toRemove []; for (let i 0; i localStorage.length; i) { const k localStorage.key(i); if (!k || !k.startsWith(PREFIX)) continue; try { const p JSON.parse(localStorage.getItem(k)); if (p p.e now p.e) toRemove.push(k); } catch (e) { toRemove.push(k); } } toRemove.forEach(k localStorage.removeItem(k)); } };这个封装里有几个设计考虑值得展开说。统一前缀app_v1_解决的是命名冲突和版本管理两件事。同源下可能同时跑着好几个应用比如主站和后台管理系统部署在同一个域的不同路径下没有前缀很容易互相踩。版本号放在前缀里是给自己留后路——将来数据结构变了直接把版本号升到v2老数据自然就失效了不用写迁移脚本。当然老数据得记得清可以在启动时遍历一遍把所有非当前版本前缀的键删掉。cleanExpired只在写入失败的时候触发是因为配额超限QuotaExceededError是唯一需要立即腾地方的时刻。平时没必要扫全表那是一次同步的遍历数据多的时候会明显卡顿。这里顺便提醒一句LocalStorage 的所有操作都是同步阻塞主线程的存一个 1MB 的字符串序列化和写盘加起来可能要几十毫秒在移动设备上更久。所以千万别在滚动、拖拽这类高频回调里写 LocalStorage也别存大对象。get里的try/catch不是摆设。我就遇到过数据被其他代码写坏的线上案例——某个版本的代码往同一个键里塞了非 JSON 的字符串之后所有读这个键的地方都在JSON.parse上抛异常整条链路全挂了。有 try/catch 兜底最坏情况是读不到走默认值不会白屏。4.3 SessionStorage 的典型用法多步表单的草稿多步表单是 SessionStorage 最典型的应用场景。用户在第一步填了一堆信息跳到第二步的时候如果没有暂存一旦刷新或者误触返回前面填的全白费用户体验极差。const DRAFT_KEY signup_draft; function saveDraft(step, data) { const draft readDraft(); draft[step] data; draft.updatedAt Date.now(); try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } catch (e) { // 容量超限通常是用户传了图片走降级不存图片只存文本 delete draft[step].files; sessionStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } } function readDraft() { try { return JSON.parse(sessionStorage.getItem(DRAFT_KEY)) || {}; } catch (e) { return {}; } } function clearDraft() { sessionStorage.removeItem(DRAFT_KEY); }为什么选 SessionStorage 而不是 LocalStorage因为这个草稿的价值只在本次填写流程里存在。用户提交成功之后草稿就是垃圾。如果放 LocalStorage用户下次再进来看到的是上次填了一半的旧数据反而会造成困惑——我是不是已经提交过了更麻烦的是如果草稿里含有敏感信息手机号、身份证号长期留在本地磁盘上是合规风险而 SessionStorage 关掉标签页就清空了风险窗口小得多。这里有个细节要注意提交成功之后一定要主动调clearDraft()。不要指望关标签页自动清就够了用户提交完不关页面、继续停留在成功页的情况很常见这段时间里草稿还在如果他又从别的入口进到表单页可能就被草稿覆盖了。主动清理是必须的。另外还有一个隐藏的坑SessionStorage 是按标签页隔离的但如果你在页面里用了iframe同源的 iframe 是可以访问父页面的 SessionStorage 的。这在微前端或者嵌入了第三方同源组件的架构里会造成意外的数据串扰做这类架构的时候要格外小心。4.4 跨标签页通信storage 事件的实际用法LocalStorage 有一个非常实用但用的人不多的能力storage事件。当一个标签页修改了 LocalStorage其他同源标签页会收到通知。注意是其他标签页触发修改的那个页面自己收不到这个不对称经常让人困惑。// 监听端任意同源页面 window.addEventListener(storage, (e) { if (e.key app_v1_theme) { applyTheme(JSON.parse(e.newValue).v); } if (e.key null) { // key 为 null 表示调用了 clear() handleAllCleared(); } }); // 触发端另一个标签页 store.set(theme, dark);这个能力特别适合处理用户在 A 标签页退出登录B 标签页还显示着登录状态这种情况。B 标签页监听到会话标识被清空立刻把界面切回未登录态避免用户点击任何按钮都报 401。实测下来有几个限制要提前知道。第一storage事件只在同源标签页之间触发跨域无效。第二如果两个标签页在同一个页面只是不同的 iframe同源的话也能触发但当心死循环——A 收到通知后如果又去写 LocalStorageB 又会收到不加判断会无限来回。我的做法是在处理函数里加一个是否已经是目标值的判断值相同就直接返回。第三无痕模式下这个事件的触发时机不太稳定别把它当成强一致的通信机制它更适合做状态提示而不是消息传递。如果需要传递更复杂的数据或者做双向通信那是BroadcastChannel的活比storage事件干净得多也不用往本地存储里写东西。不过在需要兼容老浏览器的项目里storage事件的兼容性确实更好。4.5 选型对照表拿到需求三秒做决定我把日常遇到的需求类型整理成了一张决策表遇到新需求先往这张表里对号入座基本不会选错需求类型首选方案不选其他的理由登录会话标识CookieHttpOnly Secure SameSiteLax唯一能对 JS 隐藏的客户端存储CSRF 防护令牌Cookie可读 请求头双提交服务端必须能校验用户偏好主题、语言LocalStorage需要长期保留无需发往服务端多步表单草稿SessionStorage生命周期跟流程一致敏感数据不落盘列表页的滚动位置、筛选条件SessionStorage用户回到列表页需要恢复关掉即清少量灰度/AB 标记Cookie服务端要读到才能决定返回什么大对象缓存图片、文档IndexedDBWeb Storage 塞不下且同步 API 会卡主线程跨标签页状态同步storage 事件 或 BroadcastChannel轮询成本太高不划算需要服务端随时作废的凭证Session 或带黑名单的令牌纯客户端方案做不到服务端主动失效有个判断口诀我一直在用先问服务端需不需要看到需要就往 Cookie 走再问生命周期多长跟标签页走的给 SessionStorage长期留的给 LocalStorage最后问有多大、要不要对 JS 隐藏两个都否就还是 Cookie都是就上 IndexedDB。5. 常见问题排查实录5.1 Cookie 写不进去的十种原因Cookie 相关的线上问题我处理过太多次明明设置了但前端读不到几乎是最高频的一类。按排查顺序列一下我遇到过的情况第一种是HttpOnly设了却用document.cookie去读。这个最常见看到读不到先去看响应头里的Set-Cookie有没有HttpOnly。第二种是Secure设了但当前页面跑在 HTTP 上。本地开发时代理配的是 HTTPCookie 直接被浏览器丢掉控制台里能看到黄色警告。第三种是SameSiteNone但没配Secure。新版浏览器会直接拒绝这个 Cookie而且警告信息埋得很深。第四种是跨域场景下前端没开凭证。fetch要显式写credentials: includeXMLHttpRequest 要设withCredentials true服务端还要返回Access-Control-Allow-Credentials: true并且Access-Control-Allow-Origin不能是*。这一套少一个都不行排查起来特别费劲。第五种是Domain写错。给api.example.com设 Cookie 时把 Domain 写成www.example.com直接被拒。或者试图给一个不属于自己的顶级域设 Cookie浏览器静默忽略。第六种是Path不匹配。Cookie 设了Path/api但你在/user/profile页面里读读不到是正常的。第七种是过期时间设成了过去。Max-Age0或负数是用来删 Cookie 的如果计算过期时间时有 bug 算成了负数等于刚设上就删了。第八种是名字里带了非法字符。Cookie 的名字和值都不能含分号、逗号、空格这些值里有特殊字符必须encodeURIComponent。第九种是数量超限。同域名下的 Cookie 超过浏览器上限后新写的会被静默丢弃老的也会被挤掉。这种问题特别隐蔽表现为偶尔丢登录态只有把所有 Cookie 打印出来才能发现有一堆没用的历史遗留。第十种是隐私模式或第三方 Cookie 限制。浏览器在跨站场景下对第三方 Cookie 的限制越来越严嵌入到别人页面里的组件想设 Cookie可能压根存不住。这种就只能在设计上规避别把关键状态放在第三方上下文里。排查 Cookie 问题的第一动作永远是打开开发者工具看应用面板里的 Cookie 列表再对照网络面板里的请求头响应头。看一眼Set-Cookie有没有回来、浏览器有没有真的存下来八成的问题当场就能定位。5.2 容量超限与性能问题LocalStorage 超限的表现是抛QuotaExceededError听起来很明确实际问题在于很多人没写 try/catch异常一抛后续的代码全不执行了。更糟的是这个异常在某些浏览器里会中断整个事件处理函数导致页面卡在一个奇怪的状态。处理策略我一般分三层。第一层是预防写入前估算一下大小超过阈值比如 3MB就不写走服务端或者 IndexedDB。第二层是清理超限时先扫一遍过期数据删掉再重试一次。第三层是降级还失败就静默放弃同时上报一个埋点让你知道有多少用户在触发这个问题。性能方面前面提过 Web Storage 是同步 API。这里补充一个具体的数字感受在一个中等配置的笔记本上写入一个 500KB 的 JSON 字符串大概要 20 到 40 毫秒换成低端安卓机可能到 100 毫秒以上。100 毫秒在主线程上就是明显的掉帧。所以大对象的读写一定要避开动画、滚动这类场景能延后就延后用requestIdleCallback或者setTimeout挪到空闲时段。还有一个坑是读的时候忘了删。很多页面每次进入都往 LocalStorage 里追加一条浏览历史从来不清理几个月之后用户来投诉页面卡一查本地存了几千条记录光读取就要好几百毫秒。凡是往本地存储写数据的代码都得配一段清理逻辑这是纪律。5.3 常见问题速查表现象最可能的原因排查动作登录态刷新就丢会话 Cookie 被清了或 Session 存在内存里且服务重启检查 Set-Cookie 有没有 Max-Age检查 Session 存储介质新开标签页身份丢失登录凭证存在了 SessionStorage确认凭证存储位置换 Cookie 或 LocalStorage多个标签页状态不一致依赖本地状态没做跨标签同步加 storage 事件监听上传大图后页面卡死同步写大对象到 LocalStorage改用 IndexedDB或延后写入退出登录后仍能操作只清了前端服务端 Session 还活着服务端同步销毁会话跨域请求带不上 Cookie缺 credentials 配置或 CORS 头不对前端后端两侧都核对一遍无痕模式下功能异常LocalStorage 在无痕下不持久加兜底判断降级到内存变量子域读不到主域的 CookieDomain 属性没设或设错显式设置 Domain 为上一级域偶发的登录态丢失Cookie 数量超限被挤掉清理同域下的历史废弃 Cookie6. 几个真实选型场景的推演过程6.1 场景一管理后台的登录态怎么设计假设要做一个内部管理后台前后端分离部署在同一域名下的不同路径。我的方案是这样的登录成功后服务端生成会话标识通过Set-Cookie: __Host-sidxxx; Path/; HttpOnly; Secure; SameSiteLax; Max-Age1800下发。前端完全不需要碰这个 Cookie连读都不用读。好处在哪里第一XSS 拿到了也读不到会话标识。第二自动携带意味着所有接口请求天然带着身份前端不用在每个请求上手动加 token少了一整类某个接口忘了加 header的 bug。第三服务端随时能销毁会话用户点了退出全部设备立刻生效。那前端怎么知道用户是否登录了不要靠读 Cookie 判断也读不到而是调一个/api/me接口返回当前用户信息拿到就渲染拿到 401 就跳登录页。这个模式看起来多了一次请求但状态来源唯一避免了前端以为登录了、服务端其实已经过期这种不一致。前后端完全分离、部署在不同域名的场景会麻烦一些。这时候 Cookie 得设SameSiteNone; Secure服务端 CORS 要允许凭证前端fetch要带credentials: include。这套配置能跑通但要接受第三方 Cookie 被浏览器限制越来越严的现实。如果条件允许我倾向于把前后端放到同一个父域下的不同子域然后 Cookie 设Domain.example.com这样能绕开很多跨域的麻烦。6.2 场景二界面偏好的存储策略主题、语言、字号、侧边栏折叠状态这类偏好标准答案就是 LocalStorage。它们的特点是不影响安全、不需要服务端知道、需要长期保留、体积很小。但有个细节值得说一下这类偏好最好在首屏渲染之前就读出来并应用否则会出现先闪一下白底再变黑底的观感问题。做法是在 HTML 的head里插入一小段同步脚本读完 LocalStorage 直接给html加上对应的 class等 React/Vue 挂载的时候样式已经是正确的了。这段脚本要短、要快且必须包 try/catch——万一存储里是脏数据不能让整个页面白屏。还有一个进阶做法是本地优先服务端兜底。用户换了台设备登录之后偏好应该跟着走。实现方式是登录后把服务端的偏好拉下来覆盖本地用户修改偏好时同时写本地和发请求给服务端。本地写入是为了即时响应服务端写入是为了跨设备同步。这两条路径要保证幂等用户快速切换主题的时候不会因为请求乱序导致最终状态错乱——加个时间戳服务端只接受更新的那一条就够了。6.3 场景三表单草稿与敏感信息的边界前面说过多步表单用 SessionStorage。但这里还有一个更细的边界问题如果表单里有非常敏感的信息比如身份证号、银行卡号即便是 SessionStorage 也不该存。我的判断标准是这个字段泄露之后会造成用户的实际损失吗手机号、地址这类泄露会带来骚扰但损失有限短期存在 SessionStorage 里可以接受前提是提交成功后立刻清。身份证号、银行卡、密码这类绝对不能往任何客户端存储里放只能留在内存里页面一刷新就重填。真要做刷新不丢的用户体验还有一个折中方案只存非敏感字段和当前进度。用户刷新回来敏感字段重新填其他都还在。这个体验比全丢好很多风险也可控。我在几个金融相关的项目里都是这么处理的用户反馈还不错。还有一点要注意的是合规。某些行业对用户数据的存储位置和留存时间有明确要求即便 SessionStorage 关掉就清如果页面上还嵌了第三方的统计脚本理论上它也能读到同源的 SessionStorage。所以在处理敏感业务时第三方脚本的引入要格外谨慎能延后加载就延后加载能用隔离环境的就别放在主页面里。6.4 场景四数据量大了就该换 IndexedDB 了Web Storage 是同步的、只存字符串的、5MB 上限的。这三条限制里任何一条被触碰就该考虑 IndexedDB 了。典型信号有三个要存的数据超过 2MB、需要在主线程之外操作避免卡顿、需要结构化查询按某个字段筛选、排序。IndexedDB 的学习曲线确实比 Web Storage 陡但现在的封装库已经很成熟了几行代码就能搞定基本的增删改查。我一般的做法是应用里同时保留 LocalStorage 和 IndexedDB 两套封装小的配置类数据走前者业务缓存类数据走后者各司其职。全都塞进 IndexedDB 反而会增加代码复杂度没必要。这里顺便说一个 Session 和本地存储的配合技巧。会话标识放 Cookie用户的基本信息昵称、头像、权限列表可以在接口返回后缓存到 LocalStorage带上一个较短的过期时间比如 10 分钟。页面初始化时先用缓存渲染避免白屏同时在后台发请求拿最新数据回来之后更新缓存和界面。这种缓存优先、后台刷新的模式在体验上比干等接口好太多实现成本也很低。我在实际项目里踩过最疼的一次坑是把一个是否展示引导弹窗的标记存进了 LocalStorage 却没设过期结果用户清过一次浏览器之后这个标记没了弹窗又弹出来用户以为是 bug 直接投诉到客服。后来改成带版本号的键名配合服务端记录的引导完成状态做双校验才彻底解决。这件事让我明白一个道理客户端存储永远是可以丢的任何依赖它的逻辑都必须有服务端兜底或者合理的降级路径把它当成唯一真相来源的那一刻问题就已经埋下了。最后一个实操小建议给项目里的所有本地存储键名建一个统一的常量文件写清楚每个键存什么、谁写入、谁清理、过期策略是什么。这件事花不了半小时但将来接手的人包括三个月后的你自己会省下大量猜测的时间。我现在的习惯是每个键的注释里都写一句什么时候该删事实证明这条比什么都管用。
企业数字化 ERP 产品动态
相关推荐
VC6贪吃蛇工程拆解:Win32消息循环与GDI双缓冲实战 简介:面向C初学者与课程设计学习者的贪吃蛇游戏增强版工程源码,基于VC6.0与MFC实现,适合用来理解Windows图形界面游戏的基本开发流程。工程围绕蛇体类与链表节点类的“双类”设计展开,将蛇的位置、长度与移动、增长等行为封装起来… · 2026/11/2 13:26:31
Spark二手车价格预测系统:机器学习+可视化分析实战 1. 项目概述:这不是一个“炫技Demo”,而是一套能真正跑在二手车商办公室里的分析系统我做大数据平台落地已经十多年,经手过几十个行业项目,最怕听到客户说“这个模型很准,但我不知道怎么用”。这套基于Spark的二手车市… · 2026/11/2 13:26:31
Trae AI编程助手:从代码补全到智能协同的范式跃迁 1. Trae如何颠覆传统AI编程工具范式第一次接触Trae时,我正被一个复杂的微服务架构问题困扰。与以往AI编程助手不同,Trae没有让我反复描述需求,而是直接分析代码上下文后,主动给出了包含分布式事务处理的完整解决方案。这种从"… · 2026/11/3 15:18:40
分布式文件系统核心架构与优化实践 1. 分布式文件系统概述分布式文件系统(Distributed File System, DFS)是现代计算架构中不可或缺的基础设施组件。它通过将文件存储和管理功能分布在多个服务器节点上,实现了数据的高可用性、可扩展性和容错能力。与传统的本地文件系统相比&am… · 2026/11/3 21:48:51
高性能压缩库技术选型与优化实践指南 1. 高性能压缩库的核心价值与应用场景在数据爆炸式增长的时代,高性能压缩库已经成为现代计算架构中不可或缺的基础组件。我曾在多个分布式存储项目中亲历这样的场景:当原始数据量达到PB级别时,未经压缩的传输和存储成本会呈指数级上升。这时一… · 2026/11/3 21:48:51
从零构建可审计的AI工程流水线 1. 项目概述:这不是“从零开始学AI”,而是亲手锻造AI工程流水线“ai-engineering-from-scratch”这个标题,乍看像极了某门MOOC课程的副标题,但如果你真把它当成“手把手教写神经网络”的入门课,那第一行代码还没敲&… · 2026/11/3 21:48:51
HarmonyOS RcText组件优化:多语言渲染与性能提升 1. 项目背景与组件定位在HarmonyOS 6的UI框架升级中,RcText作为文本渲染的核心组件经历了重大重构。这个历时半年的改造项目源于我们在实际开发中遇到的三大痛点:多语言混合排版时的基线对齐问题、富文本动态更新时的性能瓶颈,以及特殊字符渲… · 2026/11/3 21:48:51
Buzz技术概念解析:从声学噪声到数字传播热词 我无法根据当前输入生成符合要求的博文。原因在于:您提供的输入内容中,项目标题仅为“buzz”,且后续所有字段(项目正文、关键词、摘要描述)均为空或未提供有效信息。根据我的角色设定与创作原则,博文必须严… · 2026/11/3 21:48:51
Hadoop参数传递实战:Configuration对象与分布式缓存详解 1. Hadoop参数传递概述在Hadoop生态系统中,参数传递是开发过程中最基础也最关键的环节之一。作为一名长期使用Hadoop进行大数据处理的开发者,我深刻体会到参数传递方式的选择直接影响着作业的灵活性、可维护性和执行效率。Hadoop参数传递的本质是将配置信… · 2026/11/3 21:47:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46