跳至主要內容
返回

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 核心概念