🎯 什麼情境該想到我
當你「希望伺服器保持無狀態、好隨意加機器做水平擴展」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把整段會話狀態存在用戶端,伺服器每次請求都能無狀態處理。
做法要點:
- 把狀態序列化後存進 cookie、表單隱藏欄位或 URL 參數,隨每次請求送回伺服器。
- 伺服器不保存任何會話資料,因此任何一台都能處理任何請求,最利於叢集與容錯。
- 適合狀態量小的情況;資料越少越簡單。
★★ 今天最大規模的實踐:JWT(而它的取捨常被誤解)
★ 三種把狀態放客戶端的方式:
① ★ 隱藏表單欄位 / URL 參數 —— ★★ 完全可篡改,只能放「不重要」的東西
② ★ Cookie —— ★★ 同樣可篡改,除非簽章
③ ★★ JWT / 簽章的 cookie —— ★ 簽章讓「篡改」變成「簽章驗證失敗」
★★ 關鍵認知:簽章解決「篡改」,但不解決「撤銷」。
這是 JWT 最常被忽略的代價,也是它和 伺服器會話狀態 真正的分水嶺。
★★ JWT 的核心問題:你無法把它作廢
★ 使用者點「登出」→ 你能做什麼?
① 刪掉客戶端的 cookie → ★★ 但那個 token 仍然有效,任何人拿到它都能用
② 加一張黑名單 → ★★ 那你就有伺服器狀態了,JWT 的「無狀態」優勢消失
③ 縮短有效期(5–15 分鐘)+ refresh token → ★ 這是實務上的標準做法
→ ★★ 但 refresh token 必須可撤銷 ⇒ ★ 它一定是伺服器狀態
→ ★★ 結論:★ 真實系統是「短命的無狀態 access token + 有狀態的 refresh token」
—— ★ 純無狀態的 session 在需要登出/封鎖帳號時不存在
★ 同樣無法即時生效的還有:改密碼後踢掉舊 session、停權、變更權限。
★★ 判準:你需要「立刻讓某個 session 失效」嗎? 需要 → 你需要伺服器狀態。
★★ Go 的實作要點
// ① ★★ 一律明確指定並驗證演算法
tok, err := jwt.Parse(s, func(t *jwt.Token) (any, error) {
if t.Method.Alg() != "RS256" { // ★★ 不可省略
return nil, fmt.Errorf("unexpected alg %q", t.Method.Alg())
}
return pubKey, nil
})
// ★★ 經典漏洞:① alg=none ② 把 RS256 換成 HS256 並用公鑰當 HMAC 密鑰
// → ★ 攻擊者能自己簽出任意 token
// ② ★ 一定要驗 exp、aud、iss
// ★★ 有些函式庫預設不驗 aud —— 於是 A 服務的 token 能用在 B 服務
// ③ ★★ payload 是 base64,不是加密
// → ★ 任何人都能讀 → 絕不放 PII、內部 ID、權限細節之外的東西
// ④ ★ cookie 的旗標
http.SetCookie(w, &http.Cookie{
Name: "session", Value: tok,
HttpOnly: true, // ★★ JS 讀不到 → 降低 XSS 的傷害
Secure: true, // ★ 只走 HTTPS
SameSite: http.SameSiteLaxMode, // ★★ 降低 CSRF(但不取代 CSRF token)
Path: "/",
MaxAge: 900,
})★★ ①②是實際發生過的重大漏洞類型,而它們都是「少寫一行驗證」造成的。
★★ 大小與傳輸成本:這是真實的代價
★ JWT 帶在每個請求的 Header 上:
一個含權限清單的 token = 1–2 KB
× 每頁 30 個資源請求 = ★ 每次載入多 30–60 KB 上行
→ ★★ 而上行頻寬通常遠小於下行(行動網路尤甚)
→ ★ 對策:① token 只放 user id + 少數旗標,權限即時查(★ 但那又是伺服器狀態)
② 靜態資源走不需要認證的 CDN 路徑
③ ★★ cookie 要設 Path,不要讓它跟著每個 /static/ 請求送
★ 另外:cookie 有 4 KB 的實務上限(瀏覽器與伺服器都會拒絕過大的 header)。
★★ 什麼時候用戶端會話狀態是對的
| 適合 | 不適合 |
|---|---|
| ★ 無狀態、可水平擴展的 API | ★★ 需要即時撤銷/封鎖 |
| ★ 跨服務傳遞身分(不必每個服務查 session store) | ★ 狀態很大(購物車、多步驟表單) |
| ★ 短命的 access token | ★ 需要「目前有誰在線」 |
| ★ 無法依賴共享儲存(邊緣運算) | 需要即時變更權限 |
★★ 一個務實的混合做法:
JWT 帶 user_id 與 session_version;伺服器只存一個 user → session_version 的 map。
★ 登出/改密碼時遞增 session_version → 所有舊 token 立即失效,
而驗證成本只是一次極輕的查表(可快取)。
→ ★ 這拿到了「大部分無狀態的好處」+「可撤銷」。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
大小受限(cookie/URL 有長度上限),狀態一大就不適用。
-
安全性風險:資料在用戶端可被竄改或窺看,需加密與簽章驗證。
-
每次請求都要來回傳輸整段狀態,量大時浪費頻寬。
-
★★ 簽章解決「篡改」,但不解決「撤銷」。 需要即時登出/封鎖帳號 → 你需要伺服器狀態。
-
★★ 一律明確驗證
alg。alg=none與「把 RS256 換成 HS256 用公鑰當 HMAC 密鑰」是實際發生過的重大漏洞。 -
★★ 一定要驗
exp/aud/iss。 有些函式庫預設不驗aud→ A 服務的 token 能用在 B 服務。 -
★★ JWT payload 是 base64 不是加密,任何人都能讀。絕不放 PII。
-
★ cookie 要設
HttpOnly/Secure/SameSite,並設Path讓它不跟著靜態資源送。 -
★ JWT 帶在每個請求上是真實的上行頻寬成本(1–2 KB × 30 個請求),而上行頻寬通常遠小於下行。
-
★★ 務實的混合做法:JWT 帶
session_version,伺服器存user → session_version。 登出時遞增即可讓所有舊 token 失效。 -
★ 流程狀態(多步驟表單的當前步驟)不該放客戶端,那是可篡改的(見 應用控制器)。
-
SameSite降低 CSRF 但不取代 CSRF token。 -
cookie 有約 4 KB 的實務上限。
🔗 相關工具
- 伺服器會話狀態 Server Session State(存在伺服器端,最簡單但影響擴展)
- 資料庫會話狀態 Database Session State(存進資料庫,跨伺服器共享且可容錯)
- 企業應用架構模式
- 伺服器會話狀態 —— 可即時撤銷的對照
- 資料庫會話狀態 —— 持久且可查詢
- 應用控制器 —— 流程狀態不該放客戶端
- 值物件 —— 不可變的 token
- 遠端外觀 —— 跨服務傳遞身分