编程 localStorage 没有 5MB 保证:Web Storage 的隔离规则、配额失败与选型

2026-10-08 00:04:05

localStorage 没有 5MB 保证:Web Storage 的隔离规则、配额失败与选型

Web Storage API 提供浏览器端键/值存储,比 Cookie 直观。它有两个机制,返回不同的 Storage 对象,各自独立运行:

  • sessionStorage:为每个源(origin)维持独立的存储区,页面会话期间可用。浏览器或标签页关闭后数据清除。新标签打开页面时,会复制顶级会话上下文作为新会话上下文——这一点和 session cookie 的行为不同。打开多个相同 URL 的标签页,各自独立;同一标签页下,同源的 iframe 之间共享。
  • localStorage:跨会话保留,没有过期时间,只能通过 JS、清缓存或手动清数据清除。同一 origin 的所有标签页和窗口共享。

通过 window.sessionStorage / window.localStorage 访问。

Storage 接口

成员说明
length当前存储区中的键数量
key(index)取第 index 个键名,越界返回 null
getItem(key)读取值,键不存在返回 null
setItem(key, value)写入
removeItem(key)删除单个键
clear()清空整个存储区

键和值始终是字符串。整数 key 会自动转成字符串,存对象要先 JSON.stringify。取出来的是字符串,不是原来的类型,反序列化失败要自己兜住。

同源隔离:协议也参与

Storage 绑定到源,也就是协议 + 域名 + 端口三元组。http://example.com 和 https://example.com 是两个不同的存储对象,不同协议、不同子域也一样隔离。把站点从 http 切到 https,用户之前存的本地数据不会跟过来。

storage 事件:只发给别的标签页

Storage 对象发生变化(创建、更新、删除键)时,会在文档的 window 上触发 StorageEvent。几个容易记错的点:

  • 重复设置相同的键值,不触发事件。
  • clear() 至多触发一次。
  • 当前页面自己的改动不触发;同域下其他标签页和 iframe 会收到。
  • 事件属性:key、oldValue、newValue、url、storageArea。
  • 用 obj.key = value 这种属性赋值方式写,不触发该事件,要用 setItem。

配额:5MB 不是保证

Web Storage 不随每个请求发送到服务器,服务器也无法通过 HTTP header 操纵它,所以能存的东西比 Cookie 多。多数现代浏览器至少给 5MB(有的更多),但配额由浏览器、设备、站点策略共同决定,不要把 5MB 当成跨浏览器的保证。

几个已知的具体限制:

  • Firefox 在崩溃或重启后,每个源限制 10M。
  • 用户禁用第三方 cookie 时,第三方 iframe 不允许访问 Web Storage。
  • 隐身/隐私模式下,localStorage 被视为 sessionStorage,关闭后清空,但存储 API 仍然可用。
  • iOS 5.1+ 的移动 Safari 把 localStorage 存在 cache 文件中,系统空间不足时会被清理。

失败的表现

情况表现
配额写满setItem 抛 QuotaExceededError
来源不透明或策略禁止访问 localStorage 抛 SecurityError
隐身模式 / 第三方 iframe 被拒可用但生命周期缩短,或直接不可访问
iOS 空间不足数据被系统清理,读出来是 null

注意一个细节:仅仅断言 localStorage 是否存在也可能抛异常,所以能力探测必须包在 try/catch 里,不能靠 !!window.localStorage。

安全:同源 JS 能读走一切

localStorage 和 sessionStorage 都能被同源 JS 读取。不要把密码、长期访问令牌这类凭据放进去——一旦站点存在 XSS,攻击者直接把它们全读走。认证 Cookie 应当由服务端设置,并带上 Secure、HttpOnly、SameSite;HttpOnly 的 Cookie 无法被 JS 读取。

放哪里:三种容器的取舍

localStoragesessionStorageCookie / 服务端
生命周期无过期时间,手动清标签页会话结束即失效可设过期时间
共享范围同源所有标签页/窗口单个标签页,同源 iframe 共享同源,受 path 限制
随请求发送否否是
服务端可读否否是(HttpOnly 时仅服务端)
容量由浏览器策略决定,常见 ≥5MB同上KB 级

按这个表落地:

  • 放 localStorage:主题偏好、语言、上次打开的筛选条件、非敏感的草稿。丢了不影响登录态,重进页面能重建。
  • 放 sessionStorage:单次流程的中间状态,比如多步表单、一次性校验标记。用户新开标签页就该从零开始的东西放这里。
  • 放 Cookie / 服务端:认证态、权限、任何服务端需要校验的东西。客户端存的信息,服务端不能当事实。

代码片段

能力探测

function storageAvailable(type) {
  try {
    const storage = window[type];
    const k = '__storage_test__';
    storage.setItem(k, '1');
    storage.removeItem(k);
    return true;
  } catch (e) {
    return false;
  }
}

如果要区分失败原因,可以多看一眼异常名:QuotaExceededError 说明配额满了,SecurityError 说明这个来源根本不允许访问。

JSON 读写封装

const store = {
  set(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },
  get(key, fallback = null) {
    const raw = localStorage.getItem(key);
    if (raw === null) return fallback;
    try {
      return JSON.parse(raw);
    } catch {
      return fallback; // 值被外部改坏时不至于让页面挂掉
    }
  },
  remove(key) {
    localStorage.removeItem(key);
  },
};

写入也可能抛 QuotaExceededError,关键路径上要捕获。

跨标签页同步

window.addEventListener('storage', (e) => {
  // sessionStorage 不跨标签页共享,这里只处理 localStorage
  if (e.storageArea !== localStorage) return;

  switch (e.key) {
    case 'theme':
      applyTheme(e.newValue);
      break;
    case 'logout':
      location.reload();
      break;
  }
  // e.key / e.oldValue / e.newValue / e.url 都可读;clear() 时 e.key 为 null
});

写数据的那一侧不会收到事件,UI 需要自己同步更新一次,别指望事件回调替你做这件事。

推荐文章

程序员茄子在线接单