news 2026/10/18 1:06:57

cookie、sessionStorage、localStorage 存储选型与安全边界详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cookie、sessionStorage、localStorage 存储选型与安全边界详解

有一次帮同事排查问题,一个后台报表页面,用户前一天晚上把筛选条件、表格列配置都调好了,第二天一早打开系统,全部回到默认状态。同事第一反应是“cookie被清了”,我打开控制台看了下存储,发现他用的 sessionStorage,标签页一关,数据自然是人间蒸发。cookie、sessionStorage、localStorage 这三兄弟的区别,几乎每个前端人都背过,但真到了项目里,能一次选对的人真不多。

这篇文章我不打算给你背概念,而是从实际问题出发,把三者的底层逻辑、作用域边界、安全风险、选型判断一次讲透。不管你是刚入行的前端,还是要接手中大型项目的维护者,哪怕是准备面试,看完这篇应该都不会再被问倒。

1. 从一次“数据莫名丢失”事故说起——三个存储的相处之道

1.1 一个页面里同时存在三种存储,选错就会“背锅”

你有没有遇到过这样的场景:用户在页面上填了很长一段表单,不小心点了个刷新,内容全没了。你第一反应是“应该用 localStorage 啊”,结果代码里写的是 sessionStorage,刷新是保留了,但用户把标签页关了再打开,还是没了。

还有一种情况更隐蔽:登录之后跳转到另一个页面,token 在 A 页面能拿到,到 B 页面就变成 null。排查半天发现,写入时用的是 sessionStorage,而 B 页面是用户手动新开标签页访问的,根本拿不到 A 标签页里的数据。

这类问题有一个共同点:存储本身没坏,是选型选错了。cookie、sessionStorage、localStorage,这三个 API 都存在于浏览器里,但它们的生命周期、作用域、容量、传输方式完全不同。不理解这些差异,光靠背“cookie 小、storage 大、session 关页就没”这种口诀,到了真实项目里还是会踩坑。

所以这篇我先从底层讲起。你只有弄清楚它们各自“从哪来、归谁管、活多久”,才能在任何场景下做出正确的选择。

1.2 协议产物和浏览器 API,本质就不在同一个体系中

先说一句可能颠覆你认知的话:cookie 根本不是浏览器发明的,它是 HTTP 协议的组成部分。

HTTP 协议是无状态的,服务器无法记住“你是谁”。为了解决这个问题,早期引入了 cookie 机制:服务器通过响应头Set-Cookie在浏览器里种下一个凭证,浏览器以后每次请求都把这个凭证放进Cookie请求头里带回给服务器。所以 cookie 的生命周期、大小限制、作用域规则,很多都是从协议层面定死的。

而 sessionStorage 和 localStorage 不一样,它们是 HTML5 提供的 Web Storage API,是浏览器主动提供的“本地仓库”。它们不参与 HTTP 请求的自动发送,纯粹是给 JavaScript 在浏览器端读写数据用的。

打个比方:cookie 像是你出门必须挂在胸口的工牌,走到哪个门禁都要被扫描一下(随请求自动发送);而 Web Storage 像是你办公桌抽屉里的笔记本,方便自己查阅,但路过门口时门卫不会自动翻你的笔记本。

这个底层差异,决定了后面所有区别的走向:为什么 cookie 只有 4KB 左右,而 storage 可以到 5MB?因为 cookie 每次请求都要跟着头走,太大直接浪费带宽;为什么 storage 不需要设置过期时间,而 cookie 需要?因为 storage 本来就是一个“本地文件”,浏览器不主动清理它就一直放着。

2. cookie:身背重担的协议老兵,为什么还没退休

2.1 cookie 的真实工作流程:从 Set-Cookie 到请求头

你在浏览器控制台里执行document.cookie = "name=value",这只是在当前域名下设置了一个 cookie。但完整的 cookie 机制,尤其是服务端设置、HttpOnly、Secure 这些属性,很多人并没有亲自抓包看过。

我建议你做一个实验:打开浏览器的开发者工具,随便访问一个需要登录的网站,切到 Network 面板,点开任意一个同域请求,查看请求头里的Cookie字段,再看看响应头里的Set-Cookie字段。你会发现,服务端返回的 Set-Cookie 往往带有Path=/; HttpOnly; SameSite=Lax之类的属性。

这些属性不是摆设:

HttpOnly意思是这个 cookie 不允许被document.cookie读取,只能随请求自动发送,目的就是防 XSS 偷取身份凭证。Secure表示只允许在 HTTPS 连接下传输。SameSite则是用来限制跨站请求时是否携带 cookie 的,可以在一定程度上防止 CSRF。

我见过不少新人把 token 直接写进document.cookie,既不设 HttpOnly 也没有 Secure,结果站点被注入了一段脚本后,身份凭证被人轻松拿走。正确做法是:敏感凭证尽量让服务端设置 HttpOnly cookie,前端代码永远不应该能读到它。如果前端必须持有 token,那也至少要保证传输层是 HTTPS,并且不要放在内存之外的地方长期保存。

2.2 决定 cookie 上限的三个维度:大小、数量、域名

cookie 的大小并不是一个完全统一的值,不同浏览器实现有细微差别,但业界公认的经验值大约是4KB。什么概念?一条 cookie 存 2000 个汉字就顶天了。这里说的 4KB 是整个域名下的所有 cookie 总和,不是单条。如果你往同一个域名下塞了 50 条 cookie,每条约 80 字节,很快就到极限了。

除了大小,还有数量限制。早期规范建议每个域名下最多 20 个 cookie,现代浏览器大多是 50 个左右。超出的部分浏览器会默默丢弃,而你的代码根本不会报错,这种“静默失败”是排查问题时最头疼的。

更麻烦的是,cookie 存储在浏览器后,会以字符串形式拼接到每个请求的Cookie头里。想象一下,你的业务代码往 cookie 里塞了一堆用户偏好、浏览记录、购物车数据。用户每次点击、每次刷新、每次异步请求,这些数据都要跟着请求头来回传输。请求头被撑大了,传输变慢,服务器解析也要多花时间。

我的铁律是:cookie 只存“必要凭证”,比如 session ID、用户 ID、CSRF token,其他能不进 cookie 的坚决不进。

2.3 cookie 的路径、域名和失效机制,坑多但必须懂

cookie 的作用域规则比 storage 复杂得多,因为它在协议层面支持Domain和Path属性。

Domain决定哪些域名可以读取这个 cookie。如果你在a.example.com下设置一个 cookie 指定Domain=example.com,那么b.example.com也能拿到。注意,设置 Cookie 时 Domain 不能设为当前域之外的域,不是你想设谁就能设谁的,这是浏览器的同源保护之一。

Path决定同一域名下哪些路径可以访问。默认情况下,Path=/表示整站可用。如果你设置Path=/admin,那只有/admin及其子路径能读到这个 cookie。

失效机制也值得细说。不设置Expires或Max-Age时,cookie 是会话级 cookie,关掉浏览器就没了。设置了之后,就变成持久化 cookie,到期前一直存在。这里有个容易踩坑的点:Expires接收的是 HTTP 时间格式,如果你直接用 JavaScript 的new Date()往 Set-Cookie 里拼,格式经常对不上。优先使用Max-Age,它以秒为单位,没有格式解析问题,而且它和Expires同时存在时Max-Age优先级更高。

我还遇到过一种情况:明明调用了document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 GMT",但 cookie 还是没删掉。原因是删除时Path和Domain必须和设置时完全一致。设置时用了Path=/app,删除时没写Path,结果就删不掉。这是个很经典的坑。

3. sessionStorage 与 localStorage:双胞胎的两种宿命

3.1 写法几乎一样,命运却完全不同

从使用者的角度看,sessionStorage 和 localStorage 简直是一对双胞胎:同样的 API,setItem、getItem、removeItem、clear、key,连存储格式都是键值对字符串。很多人因此觉得它们“差不多”,只是在“关闭标签页会不会清空”上有所不同。

但如果只记住这一点,你会在更隐蔽的地方踩坑。

先说共性:两者都只能存储字符串。你要存对象,必须自己JSON.stringify转成字符串,读出来再JSON.parse解析回对象。如果你直接setItem('obj', {a: 1}),存进去的会是"[object Object]"。另外,它们的容量大约是5MB 或 10MB(不同浏览器有差异),远超 cookie 的 4KB,但也远小于后端数据库。

再说差异:sessionStorage 的生命周期绑定标签页。什么概念?不是“关闭浏览器”,而是关闭标签页。你在地址栏输入同一个网址,回车打开一个新标签页,和原来的标签页是完全隔离的。如果你用window.open(url)打开新标签页,只要新页面是通过脚本弹出的,它会复制一份 sessionStorage 过来,但两者从此互不相干。

localStorage 则简单粗暴:没有过期时间,没有标签页概念,同源下的所有页面共享同一份数据,除非你手动清掉或代码删除,否则它会一直霸占空间。

3.2 sessionStorage 的作用域陷阱:标签页隔离

sessionStorage 最容易被低估的是它的“标签页隔离”属性。很多人以为它是“会话级”的,觉得浏览器没关就一直能用。实际上,它的生命周期是“标签页级”。

举个例子:你的系统有个列表页,点进去是详情页。用户在列表页设置了一组筛选条件,存到 sessionStorage,然后跳到详情页。此时因为详情页是同一个标签页内通过路由跳转的,sessionStorage 可以读到。但如果用户从详情页复制链接,再开一个新标签页访问列表页,那筛选条件是读不到的。因为这是一个全新的标签页,存的是另一份空数据。

iframe 也是类似的情况。如果页面里有 iframe 加载同源页面,它们可以共享 sessionStorage,但跨域 iframe 就不行。虽然这个场景不多,但碰到了也够你排查一阵子。

sessionStorage 最适合的场景,是那些“开了一个标签页就要用,关了就拉倒”的临时数据,比如表单草稿:用户填到一半刷新页面,草稿还在;用户彻底关闭页面,草稿自动清理,不用你操心过期时间。

3.3 localStorage 的持久性:既是福利也是坑

localStorage 能永久保存,这既是它的优势,也埋着不少隐患。

第一个隐患是“数据结构老化”。你的业务代码可能在一期版本里往 localStorage 里存了{version: 1, data: ...},到了三期版本,代码里改成读{version: 2, items: [...]}。老用户浏览器里还留着一期的旧数据,页面一打开就解析报错。这时候你得写一套迁移逻辑,或者至少要在读写时做结构校验。

第二个隐患是“空间滥用”。localStorage 虽然单个域名有 5MB 左右的额度,但如果你什么数据都往里扔——操作日志、埋点队列、图片 base64、接口缓存——用着用着就到顶了。而且 localStorage 的写入是同步的,往里面塞大字符串时,页面甚至会卡顿。我见过一个运营后台,每次搜索完都把整个结果列表存进 localStorage,列表几千条数据,存一次要几百毫秒,页面明显卡顿。

第三个隐患是“清理无门”。用户清浏览器缓存时,一般会连 localStorage 一起清掉,你的“永久登录”状态可能就会丢。如果你把重要的 token 存在 localStorage,用户清除浏览器数据后就会被踢下线,这是很多“为什么我清了缓存还要重新登录”问题的根源之一。

我有一个习惯:给 localStorage 里的每类数据加一个前缀,并存入版本号。比如myapp:user_pref:v1。这样后续升级、清理、调试都能方便很多。

4. 三个维度的碰撞:生命周期、容量与作用域到底差在哪

4.1 一句话记住三者的区别

到这一步,我们可以把三者的核心差异放在一起看:

维度cookiesessionStoragelocalStorage
本质HTTP 协议产物浏览器本地存储 API浏览器本地存储 API
存储大小约 4KB(域名总限额)约 5MB 以上约 5MB 以上
生命周期可设置 Expires/Max-Age,不设则为会话级标签页关闭即销毁永久保存,需手动清理
传输方式每次 HTTP 请求自动携带不随请求发送不随请求发送
作用域按域名 + 路径(可跨子域)同源 + 同标签页同源(协议+域名+端口)
默认隔离按域名标签页间隔离同源共享
可被服务端设置是(Set-Cookie)否否

这个表格建议你存下来,面试和平时遇到选型问题,先过一遍这个表,基本不会错。

4.2 同源的三位一体:协议+域名+端口

Web Storage 的作用域“同源”到底怎么理解?“源”是由三部分组成的:协议、域名、端口。任何一项不同,都不算同源,都不能共享 localStorage。

举个实际的坑:你的站点在http://example.com下运行,用户把 http 访问习惯改成手动输入https://example.com,此时浏览器会认为http和https是两个完全不同的源。localStorage里的数据在新的源下面是空的,页面上的“记住我”功能就失效了。如果你同时接入了 HTTP 和 HTTPS 两套环境,要格外注意 storage 的隔离问题。

子域名也是重灾区。假设admin.example.com和www.example.com是两个应用,它们各自访问各自的localStorage,互不共享。如果你的业务需要主站和子站之间共享登录态,那通常还是得靠 cookie,因为 cookie 可以指定Domain=example.com,让所有子域名都能读到。代价是,这个 cookie 也会被发送到所有子域名的请求里,安全边界变宽了。

记住一句话:localStorage 是同源共享,sessionStorage 是同源同标签页共享,cookie 是“域 + 路径”共享。三者作用域的范围并不相同。

4.3 什么时候选哪个?先回答三个问题

与其背“cookie 用来存 session,storage 用来存缓存”,不如在每次决定用哪个存储之前,先问自己三个问题:

问题一:这个数据需要发给服务端吗?如果需要,cookie 是天然候选,因为它是唯一能随 HTTP 请求自动携带的。但注意,不要存大体积数据,只放凭证类信息。如果服务端只需要在特定时机拿到数据,那也可以用 storage 存,前端手动放到请求体里发送。问题二:关闭标签页之后,这个数据还需要存在吗?不需要,选 sessionStorage;需要,选 localStorage 或持久化 cookie。问题的关键就是这个页面是“用完即走”还是“长期复用”。

问题三:需要多个页面共享吗?在一个标签页内路由跳转之间共享,sessionStorage 就够;跨标签页共享,localStorage 才是正解;跨子域共享,只能靠 cookie。

这三个问题走完,90% 的选型场景都有答案了。

5. 被低估的安全边界:XSS、CSRF 与存储选择的攻防关系

5.1 cookie 是 CSRF 的天然靶子

很多初学者会困惑:cookie 这么不方便,为什么不用 localStorage 存一切?“每次请求都自动带上”这个特性,是把双刃剑。它带来了便利,也带来了 CSRF(跨站请求伪造)风险。

简单说,CSRF 的攻击方式是:你在 A 网站已登录,浏览器里存着 A 网站的登录 cookie。你又在另一个网站上点了某个恶意链接,这个链接指向 A 网站的某个敏感接口。由于 cookie 会被浏览器自动携带,A 网站收到请求后会认为这是你本人操作。

要防御 CSRF,单纯靠存储选型是不够的,它需要后端配合。常见做法有校验Referer、使用自定义请求头、使用 CSRF token。现代浏览器也提供了SameSite属性,设置为Lax或Strict后,跨站请求就不会自动携带 cookie。这也是为什么现在越来越多的网站在设置登录 cookie 时,都会加上SameSite=Lax。

所以在选型时,你要有个意识:cookie 作为凭证载体的前提是,服务端必须有能力防御 CSRF。如果你们的接口主要是内部管理系统,且不打算做复杂的 CSRF 防护,那可以考虑把 token 放到 localStorage 里,由前端在请求头里手动加Authorization字段。这样至少不会因为“cookie 自动携带”就产生跨站漏洞。

5.2 localStorage 和 XSS 的裙带关系

localStorage 不是没有安全问题,它最大的软肋是 XSS(跨站脚本攻击)。

XSS 攻击如果得手,攻击者可以在你的页面里执行任意 JavaScript。这时候,localStorage 里的所有内容都像摆在桌面上的文件一样,可以随便翻阅。如果你的 token、用户信息放在 localStorage,攻击者一条fetch就能把它们偷走。

相比之下,cookie 有一个 localStorage 没有的保命技能:HttpOnly。设置了 HttpOnly 的 cookie 不能被document.cookie读取,所以即使页面被注入脚本,攻击者也偷不走这个 cookie。

这就是为什么“token 到底放 cookie 还是 localStorage”会争论这么久。放 cookie,你有 CSRF 问题;放 localStorage,你有 XSS 风险。在我看来,不存在绝对安全的选项,只有匹配你防御体系的选项:

  • 如果你们的接口有完善的 CSRF 防护,且登录 cookie 设置了 HttpOnly + SameSite,那 cookie 更稳。
  • 如果你们是前后端分离、以 Authorization 请求头传递 token 的架构,并且代码里对 XSS 有专门防护,那 localStorage 也能用,但要定期清理、加过期时间。

最怕的是“人云亦云”:网上说 localStorage 不安全就一律不碰,或者图省事全扔 localStorage。安全是个系统工程,存储选型只是中间一环。

5.3 不同存储的安全等级与取舍

攻击类型cookiesessionStoragelocalStorage
XSS 偷取可由 HttpOnly 缓解可被脚本读取可被脚本读取
CSRF 利用受 SameSite 和 CSRF 防护影响不自动发送,风险低不自动发送,风险低
数据被第三方脚本读取看是否有 HttpOnly同源页面脚本均可读同源页面脚本均可读
清除浏览器数据后丢失是是是

有一个容易被忽视的隐患是第三方脚本。现在很多站点会用统计脚本、客服插件、埋点 SDK,这些第三方脚本在页面里执行时,同样可以访问 localStorage 和 sessionStorage(只要它们在同一个 Document 下)。所以不要把敏感信息随便存进 storage,尤其是你没有完整审计过第三方依赖的项目。

我在项目里养成的一个习惯是:storage 里可以放 UI 状态、用户偏好、非敏感缓存,但不放密码、不放长期 token。如果必须放 token,就设一个短的过期时间,并且每次读取时校验签发时间,超过阈值强制重新登录。

6. 面试与实战都能用的选型清单

6.1 60秒速记表

如果面试官让你现场说区别,你可以按这个节奏答:

  • 从本质讲:cookie 是 HTTP 协议的一部分,自动随请求发送;Web Storage 是浏览器 API,纯本地存储。
  • 从容量讲:cookie 约 4KB,storage 约 5MB 起。
  • 从生命周期讲:cookie 由服务端或 JS 通过 Expires/Max-Age 控制;sessionStorage 关标签页即销毁;localStorage 永久保存。
  • 从作用域讲:cookie 可按 Domain 跨子域、按 Path 限定路径;storage 受同源限制,其中 sessionStorage 还多一层标签页隔离。

按这四条答下来,比死背概念要稳得多,因为“为什么”的逻辑是自洽的。

6.2 一份可以直接抄的 Storage 封装

你如果经常在项目里直接调 localStorage,我建议你花 10 分钟封装一个模块。下面这个是我常用的结构,带命名空间、JSON 序列化和过期时间:

const STORAGE_PREFIX = 'myapp'; function buildKey(key) { return `${STORAGE_PREFIX}:${key}`; } export const storage = { set(key, value, maxAgeSeconds) { const data = { v: 1, value, expiresAt: maxAgeSeconds ? Date.now() + maxAgeSeconds * 1000 : null }; localStorage.setItem(buildKey(key), JSON.stringify(data)); }, get(key) { const raw = localStorage.getItem(buildKey(key)); if (!raw) return null; try { const data = JSON.parse(raw); if (data.expiresAt && Date.now() > data.expiresAt) { localStorage.removeItem(buildKey(key)); return null; } return data.value; } catch (e) { // 结构损坏时兜底清理,避免页面报错 localStorage.removeItem(buildKey(key)); return null; } }, remove(key) { localStorage.removeItem(buildKey(key)); }, clear() { // 只清理自己前缀下的 key,避免误删其他模块数据 Object.keys(localStorage) .filter((key) => key.startsWith(STORAGE_PREFIX)) .forEach((key) => localStorage.removeItem(key)); } };

这段封装解决了我说的几个痛点:带前缀避免和其他功能冲突,带版本字段方便以后迁移,带过期时间强制清理。你如果对 sessionStorage 也有类似需求,把localStorage换成sessionStorage即可。

6.3 我的实战建议清单

项目里具体的选型,我一般按下面的规则来,你可以参考:

登录态和身份凭证:优先交给服务端设置 HttpOnly cookie,并配置 SameSite。如果架构上必须前端持有 token,也存在内存变量里,别长期落盘。用户偏好设置(主题、语言、列表视图):localStorage。这类数据不敏感,需要跨页面长期保留。表单草稿:sessionStorage。用户关闭标签页后草稿就该消失,免得下次进来还看到旧内容。跨标签页通信:localStorage 加storage事件。注意这个事件在其他标签页修改数据时才会触发,当前标签页不会触发,需要自己处理。接口缓存:localStorage 可以,但记得设过期时间、控制体积,最好再加上容量上限检查,超过阈值就直接不缓存。

什么场景不该用哪类存储,也顺便提醒一下:

  • 不要在 cookie 里存业务数据,请求头会膨胀。
  • 不要把整个用户对象塞进 localStorage,真要存就存一个不敏感的 ID,其他信息每次从接口拉。
  • 不要在一个标签页里给 sessionStorage 存大量数据,因为新开标签页并不会自动带过去。

我自己维护的项目里,凡是存储选型没想清楚的,过一阵子一定会有人过来说“数据莫名其妙没了”或者“为什么这个页面不共享数据”。这些问题的根源,基本都能在这篇文章里找到答案。

最后再分享一个调试小技巧:控制台的 Application 面板(或者旧版叫 Resources)里,可以分门别类地查看 Cookie、Local Storage、Session Storage。遇到数据丢失问题,第一时间去这里看一眼,比在代码里瞎找一个小时靠谱得多。存储这块,你理解得越透彻,排查问题就越从容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 1:06:11

MAX30102与ESP32硬件协同设计:电源完整性、I²C时序与血氧算法实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 1:04:26

PDU Modbus采集超时排查:先别急着换网关

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 1:04:12

USART串口通信从入门到调试:帧格式、波特率与常见问题排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 1:03:59

汽车电子芯片选型:从参数对比到系统级失效权衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 1:03:57

RFID芯片选型避坑:CLRC663 Pin-to-Pin兼容替换的成本与迁移评估

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华