🎯 什麼情境該想到我

當你「希望伺服器保持無狀態、好隨意加機器做水平擴展」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:把整段會話狀態存在用戶端,伺服器每次請求都能無狀態處理。

做法要點:

  • 把狀態序列化後存進 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 立即失效,
而驗證成本只是一次極輕的查表(可快取)。
→ ★ 這拿到了「大部分無狀態的好處」+「可撤銷」。

★ 兄弟辨析

差別
伺服器會話狀態★ 狀態在記憶體/共享儲存;★ 可即時撤銷
資料庫會話狀態★ 狀態在資料庫;持久、可查詢、可分析
應用控制器★★ 流程狀態不該放客戶端(可篡改)
值物件簽章的 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 的實務上限。

🔗 相關工具