跳到主要内容
返回

Local Storage、Session Storage 与 Cookies

前端

实务指南:browser storage、其取舍,以及 production 中真正重要的安全边界

Browser storage 看起来简单,直到它成为 authentication flow、multi-tab 体验,或必须撑过 schema changes 的应用的一部分。


  • API 很少是困难之处。真正的决策是 谁需要这些数据、它应该存活多久,以及它跨越了哪条安全边界。
  • 浏览器并没有名为 cookieStorage 的标准 API。传统 cookies 走 document.cookie;较新的异步 Cookie Store API 走 cookieStore。它们管理的是同一层底层 cookies,两者的行为都不像 Web Storage。

概览

PropertylocalStoragesessionStorageCookies
Typical lifetimeUntil explicitly cleared or evictedCurrent tab's page sessionSession-based or until their expiry date
Sent to serverNoNoYes, on matching requests
JavaScript accessYes, synchronouslyYes, synchronouslyYes, unless marked HttpOnly
Best suited forPersistent, non-sensitive preferencesTemporary state that should survive reloadsServer-managed sessions and small server-readable values

  • Web Storage 属于 client。Cookies 参与 HTTP request lifecycle。
  • 谁都不该被当成受信任的 database。


localStorage

localStorage 把数据为某个 origin 持久保存,撑过 page reloads 与 browser restarts。


ts
const preferences = {
  theme: "dark",
  density: "compact",
}

localStorage.setItem("preferences:v1", JSON.stringify(preferences))

const stored = localStorage.getItem("preferences:v1")
const parsed = stored ? JSON.parse(stored) : null

ts
type Preferences = {
  theme: "light" | "dark"
}

function readPreferences(): Preferences | null {
  try {
    const value = JSON.parse(localStorage.getItem("preferences:v1") ?? "null")

    if (value?.theme === "light" || value?.theme === "dark") {
      return value
    }
  } catch {
    // Corrupt or manually edited storage should not break the application.
  }

  return null
}

  • 适合少量、非敏感 preferences:theme、已关闭的 notice、未完成的 local draft。
  • Values 是 strings,访问是 synchronous,每一次 read 或 write 都会挡住 main thread。Capacity 与 eviction 因 browser 而异。它是小型 persistence 机制,不是 database。
  • Version keys,并在读取时 validate:已部署的应用会变,旧的 browser 数据还在。
  • Failure: 把 session tokens、passwords,或长期 bearer tokens 放进 localStorage。该 origin 上的任何 script 都能读它,包括 XSS。偷走这个值,就足以在别处重用。


sessionStorage

sessionStorage 有类似的 string-based API,但它的 lifetime 绑在 page session。


ts
sessionStorage.setItem(
  "checkout:draft",
  JSON.stringify({ step: 2, deliveryMethod: "pickup" })
)

  • 它能撑过同一 tab 里的 reloads,通常在该 tab 或 window 关闭时被清除。分开的 tabs 得到分开的 storage。
  • 适合临时 UI state:multi-step form、return URL,或应撑过一次意外 refresh、却不该变成永久 preference 的 filters。
  • Failure: 把 sessionStorage 当成安全边界。页面上的 scripts 仍然能读它,用户也可以用复制或恢复 tabs 的方式,让 lifetime 假设变得没那么明显。它适合的是 product lifetime 与数据匹配的时候,而不是因为它比 localStorage 更安全。


Cookies 不同,因为 browser 可以把它们附在 HTTP requests 上。


http
Set-Cookie: session=opaque-value; Path=/; HttpOnly; Secure; SameSite=Lax

ts
const preference = await cookieStore.get("theme")

await cookieStore.set({
  name: "theme",
  value: "dark",
  path: "/",
  sameSite: "lax",
})

  • 适合 server 需要这个值的时候,尤其是 server-managed sessions。Security-sensitive cookies 通常应由 server 创建。
  • HttpOnly 阻止 JavaScript 读取 cookie,Secure 把它限制在 HTTPS,SameSite 有助于限制 CSRF。它们降低风险;它们不能取代 output escaping、CSP、CSRF analysis,或 session expiration 与 rotation。
  • 传统 document.cookie 是 synchronous 的,解析也很别扭。Cookie Store API 在支持的地方是 asynchronous。Client-side code 仍然无法通过 cookieStore 读取 HttpOnly cookie——这正是限制的意义。
  • Failure: 因为 cookieStore 看起来像 localStorage,就把 preferences 塞进 cookies。Cookies 很小,domain 与 path 规则很微妙,而且随 requests 带上的 cookies 每次都会增加网络开销。


默认选择

以最短的有用 lifetime,存放最少的数据。


  • 当数据只需在当前页面打开期间存活时,使用 in-memory state。
  • 当临时 state 应在单一 tab 中撑过 reloads 时,使用 sessionStorage。
  • 对应需要跨多次访问持久保存的少量、非敏感 preferences,使用 localStorage。
  • 当 server 必须收到该值时使用 cookies,尤其是放在 secure、HTTP-only cookie 中的 opaque session identifier。
  • 当应用需要大量数据、结构化 records、transactions,或 offline 行为时,改用 IndexedDB。
  • Browser storage 由用户控制,可被清除或修改,绝不应被当成权威的 source of truth。读取时验证、处理 storage failures,并让 server-side authorization 独立于 client 可编辑的任何内容。

选择 storage 是架构决策,而不是 API 偏好。从数据的 lifetime 与信任等级出发;正确的 browser primitive 通常会随之而来。


Recap Q&A

阅读下一篇笔记
JavaScript 核心概念