web cookie session localStorage sessionStorage http

[Web] Cookie、Session、localStorage 與 sessionStorage

HTTP 是無狀態的

HTTP 是無狀態協定(stateless protocol)——每一次請求對 server 來說都是獨立事件,server 認不出「這次請求」和「上次請求」是不是同一個人發的。

沒有額外機制的話,後果很直接:使用者每換一頁就得重新登入,購物車這種跨頁面的狀態也留不住。

為了補上「狀態」這塊,前後端發展出幾種機制:核心是 CookieSession,瀏覽器端的 localStoragesessionStorage 則是後來的延伸。

Cookie 是 Netscape 工程師 Lou Montulli 在 1994 年發明的,是最早解決 HTTP 無狀態問題的方案。

它的本質很簡單:一小段由 server 寫入、存在瀏覽器裡的文字資料。之後瀏覽器每次對同一個 server 發請求,都會自動把對應的 cookie 附在 HTTP request header 上,server 一看就知道這個請求是誰發的。

  1. 使用者登入,server 驗證帳密成功後,在 HTTP response header 寫入 Set-Cookie
  2. 瀏覽器收到後,自動把 cookie 儲存起來。
  3. 之後每次對同一 domain 的請求,瀏覽器都會自動在 request header 帶上 Cookie
  4. Server 讀取 cookie,識別使用者身份或取得需要的狀態資訊。
屬性說明
Expires / Max-Age設定 cookie 的有效期限。沒設的話就是 session cookie,瀏覽器關掉就消失
Domain限制哪些 domain 可以存取這個 cookie
Path限制哪些路徑下的請求會帶上這個 cookie
HttpOnly設定後,JavaScript 無法透過 document.cookie 存取這個 cookie
Secure設定後,只有 HTTPS 連線才會傳送這個 cookie
SameSite控制 cookie 能不能在跨站請求中被帶上,可設 StrictLaxNone

HttpOnly 是防 XSS(Cross-Site Scripting) 的關鍵。

  • XSS 的玩法:攻擊者把惡意 JavaScript 注入網頁,用 document.cookie 把使用者的 cookie 偷走,拿去假冒身份
  • 設了 HttpOnly,JavaScript 根本讀不到那個 cookie,惡意腳本無從下手
  • Secure 則確保 cookie 只在 HTTPS 加密連線下傳送,不會在明文傳輸時被中間人攔走

登入相關的 cookie,一律 HttpOnly + Secure

大多數情況下不需要。 登入驗證相關的 cookie 由後端設定與讀取,加上 HttpOnly 之後 JavaScript 也碰不到。

前端真的會碰 cookie 的情境不多:

  • 紀錄語言偏好或主題設定(但這種需求現在更常用 localStorage
  • 第三方 SDK(廣告追蹤、analytics)自己寫入的 cookie

瀏覽器端用 document.cookie 讀寫 cookie(前提是該 cookie 沒設 HttpOnly):

// 設定 cookie(expires 以 UTC 格式字串表示)
document.cookie = 'username=Jeremy; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/'

// 讀取所有 cookie(是一整個字串,需要自己解析)
console.log(document.cookie) // "username=Jeremy; theme=dark"

// 刪除 cookie(把 expires 設成過去的時間)
document.cookie = 'username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'

document.cookie 讀出來是一整條字串,要自己 parse,用起來不太舒服。現代專案如果真的要操作 cookie,通常直接用 js-cookie。至於 HttpOnly cookie,JavaScript 讀不到,本來就是後端的職責。

Session

Session 解的也是 HTTP 無狀態問題,跟 cookie 最大的差別在:session 資料存在 server 端,不是 client 端

Session 的運作流程

  1. 使用者登入,server 驗證成功後,在 server 的記憶體(或 Redis、資料庫)建立一筆 session 資料,並產生一個唯一的 Session ID
  2. Server 把這個 Session ID 透過 Set-Cookie 傳給瀏覽器(是的,Session ID 通常就是存在 cookie 裡)。
  3. 之後每次請求,瀏覽器帶上這個 Session ID 的 cookie,server 就根據 Session ID 找到對應的 session 資料,藉此識別使用者。

所以 session 和 cookie 不是兩個互斥的東西——session 通常就是靠 cookie 來傳 Session ID。差別在於資料放哪:cookie 把資料放在 client,session 把資料留在 server,client 手上只有一把「鑰匙」(Session ID)。

登入後,server 回傳的實際 HTTP header 大致長這樣:

HTTP/1.1 200 OK
Set-Cookie: connect.sid=s%3A7xT...abc; Path=/; HttpOnly; Secure; SameSite=Lax

後續請求瀏覽器會自動帶上:

GET /profile HTTP/1.1
Cookie: connect.sid=s%3A7xT...abc

Express 搭配 express-session 的最小實作:

import express from 'express'
import session from 'express-session'

const app = express()

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}))

app.post('/login', (req, res) => {
  req.session.userId = 42  // session 資料存在 server,cookie 只帶 session id
  res.sendStatus(200)
})

app.get('/profile', (req, res) => {
  if (!req.session.userId) return res.sendStatus(401)
  res.json({ userId: req.session.userId })
})

JWT 的運作流程

傳統 server-side session 在 MPA 架構下運作得很好,但 SPA 與前後端分離普及之後,問題開始浮現:

  • 狀態集中在 server:每個登入中的使用者都要在 server 維護一筆 session 資料,使用者一多,記憶體就吃緊
  • 橫向擴展困難:使用者登入時 session 建在 A server,下一個請求被路由到 B server,找不到 session,就被登出了
  • 解法不是沒有——sticky session(固定路由到同一台 server)或共享 session store(如 Redis)——但都是額外的架構成本

JWT(JSON Web Token) 是 token-based 的驗證方式,核心思路跟 session 正好相反:server 不儲存任何狀態。

  1. 使用者登入,server 驗證成功後產生 JWT token,裡面放使用者 ID、角色、有效時間等資訊,回傳給前端。
  2. 前端存放 token,兩種主流做法:
    • HttpOnly cookie:瀏覽器自動管理,JavaScript 無法存取,安全性較高,但要處理 CSRF。
    • 記憶體(框架 state/store):refresh 頁面就消失,要搭配 refresh token 機制續期,但不怕被 XSS 偷走。
  3. 每次請求時,前端把 token 放進 Authorization header(格式 Bearer <token>)。
  4. Server 只要驗證 token 簽名是否合法就能確認身份,不用查資料庫或記憶體。任何一台 server 都能獨立驗證,不需要共享 session store。

JWT 看起來像一串亂碼,但其實結構是三段 Base64 編碼用 . 串起來的:header.payload.signature

  • header:說明這個 token 用的是什麼簽名演算法
  • payload:實際儲存的資訊,例如使用者 ID、角色等(這段是可以被解碼讀取的,所以不要放密碼或任何敏感資料)
  • signature:用 server 的 secret key 對前兩段做簽名,確保內容沒有被竄改

所以 JWT 的安全性不在於「資料被隱藏了」,而在於「資料如果被竄改,簽名就會對不上,server 就會拒絕這個 token」。

Session vs. JWT 怎麼選

Session 有個特性是 server 可以主動讓它失效——只要把那筆 session 從記憶體或 Redis 刪掉,使用者馬上就被登出。 但 JWT 因為 server 不記狀態,沒辦法這樣做。 Token 一旦發出去,在有效期間內根本沒辦法讓它失效,除非另外維護一個 token 黑名單,但那又繞回需要共享狀態的問題了。

所以兩種方案各有取捨,沒有絕對的優劣:

  • 需要嚴格的即時登出控制(例如金融、醫療類應用)?Session 可能更合適。
  • 服務是前後端分離、多服務架構,或有 mobile app 要一起接?JWT 通常更方便。

localStorage 與 sessionStorage

Cookie 拿來在瀏覽器端存資料,有個相當惱人的限制:每個 cookie 最多 4KB,而且每次 HTTP 請求都會自動帶上,白白吃頻寬。

HTML5 帶來了 Web Storage API,提供兩個更現代的瀏覽器端儲存方案:localStoragesessionStorage。 兩者的 API 幾乎一模一樣,差別只在生命週期範圍

localStorage

localStorage 存的資料是持久性的,瀏覽器關掉也不會消失,除非手動清除或程式碼主動刪除。 資料的範圍以 origin(protocol + domain + port) 為單位,同一個 origin 下的所有頁面都讀得到。

localStorage.setItem('theme', 'dark')

const theme = localStorage.getItem('theme') // "dark"

localStorage.removeItem('theme')

localStorage.clear()

sessionStorage

sessionStorage 的 API 跟 localStorage 完全相同,但生命週期只在當前的瀏覽器分頁(tab)開著的期間。 關掉分頁或瀏覽器,資料就消失了。 而且更嚴格的是,就算是同一個 origin,不同分頁之間的 sessionStorage 也是各自獨立、互不共享的。

sessionStorage.setItem('step', '2')
const step = sessionStorage.getItem('step') // "2"
sessionStorage.removeItem('step')
特性CookielocalStoragesessionStorage
容量上限~4KB~5-10MB~5-10MB
生命週期可自訂(或 session)永久,直到手動清除當前分頁存活期間
跨分頁共享是(同 origin)是(同 origin)
隨 HTTP 請求傳送是,自動帶上
JavaScript 可讀可(除非 HttpOnly)
伺服器可設定

不要把 JWT 或任何 authentication token 存在 localStorage

這是一個相當常見的誤區,尤其是網路上有很多教學直接示範把 JWT 塞進 localStorage,然後每次 request 再從裡面拿出來放進 header 帶走。 問題在於,localStorage 可以被頁面上任何 JavaScript 存取,只要頁面有 XSS 漏洞,攻擊者就能輕鬆把 token 偷走,完整模擬使用者的身份,而且因為 JWT 本身 server 不記狀態,你也很難即時讓那個被盜的 token 失效。

相對安全的做法是把 JWT 存在設有 HttpOnly 的 cookie 裡,讓 JavaScript 碰不到它,由瀏覽器自動附上,由後端讀取驗證。 當然,用 cookie 傳遞 token 又要注意另一種攻擊:CSRF (Cross-Site Request Forgery),也就是惡意網站誘騙瀏覽器發出帶有你 cookie 的請求。 應對 CSRF 通常搭配 SameSite cookie 屬性或 CSRF token 機制來處理。

說到底,Web 安全沒有銀彈,每種方案都有自己的攻擊面,重點是要清楚知道自己在用什麼、它保護了什麼、又暴露了什麼。

localStorage 適合存什麼?

localStoragesessionStorage 因為 JavaScript 可以直接讀寫,非常適合用來存放不含敏感資訊的 UI 狀態,例如:

  • 使用者的外觀偏好(深色/淺色主題)
  • 語言設定
  • 表單的暫存草稿
  • 使用者非敏感的個人設定

說白了,就是「放這裡被偷走也沒什麼大不了」的東西。

Summary

一個簡單的場景比喻,幫助記憶這幾個概念的差異:

想像你去一家餐廳:

  • Cookie 就像餐廳給你一張會員卡,每次來都帶著,服務生一掃就知道你是誰(資料可能直接寫在卡上,或只是一個 ID)。
  • Session 就像餐廳只給你一個號碼牌,你的點餐記錄、座位偏好全都存在餐廳後台,號碼牌只是讓服務生查資料用的。
  • localStorage 就像你手機裡記的備忘錄,下次來餐廳前可以先查一下自己上次喜歡吃什麼,但餐廳本身不知道也不負責這份備忘錄。
  • sessionStorage 就像這頓飯吃完的點菜記錄,離開餐廳就清空了。

後端怎麼把憑證交給前端?

這個問題很多新手沒有搞清楚,常常以為「登入 API 回來就是登入完成」,但其實憑證(cookie、session ID、JWT)是透過不同的管道傳遞的,而不是都塞在 API 的 response body 裡。

機制後端怎麼傳前端怎麼收
Cookieresponse header 的 Set-Cookie瀏覽器自動存入,前端完全不介入
Session ID同上,本質還是 Set-Cookie同上,瀏覽器自動處理
JWT(cookie 方式)response header 的 Set-Cookie(含 HttpOnly瀏覽器自動存入,前端完全不介入
JWT(body 方式)response body 的 JSON,例如 { token: "..." }前端從 response body 裡撈出來,自己決定存哪

關鍵就在最後兩行:JWT 後端可以選擇兩種回傳方式,行為完全不同。 用 Set-Cookie 傳的話,跟 cookie / session 一樣,前端感知不到、也不需要處理,瀏覽器自動把它存起來,之後每次請求也自動帶上。 但如果後端選擇放在 response body 裡,前端就必須自己去讀取、自己決定存在哪(記憶體、localStorage 等),也要自己在之後的請求手動帶上 Authorization header。

所以當你跟後端討論驗證機制時,要確認的不只是「我們用 JWT 還是 session」,還要確認「JWT 是從 header 來還是 body 來」——這直接決定了前端要不要自己處理存取邏輯。

現代實務怎麼用?

登入驗證:現代前後端分離的專案,主流是 JWT token-based 驗證。 token 的存放方式有兩派,一派存 HttpOnly cookie(安全性較高,但要處理 CSRF),另一派存在框架的 state/store 記憶體裡,每次 refresh 頁面就消失,需要搭配 refresh token 機制來自動續期。 這兩種方式各有取捨,不同團隊的選擇不同。

前端基本不碰跟驗證有關的 cookie:正常的開發流程中,前端工程師幾乎不會需要直接操作 cookie 來處理登入相關的邏輯,這塊交給後端設定 HttpOnly cookie,前端感知不到也不需要感知。 前端能碰的 cookie,通常只剩第三方 SDK 自己寫的追蹤或 analytics 類 cookie,或者極少數需要前端主動設定的偏好類資料(但這種情境現在也幾乎都改用 localStorage 了)。

UI 狀態的儲存:深色模式、語言偏好、側欄展開收合狀態… 這類「跟使用者帳號無關的個人設定」,幾乎一律放 localStorage。 如果是多步驟表單的暫存進度、或只需要在這次操作中保留的資料,則會考慮 sessionStorage

Latest Updates

  • 2026.06.11 Content updated