JWT 不要存 localStorage,我是認真的

一句話總結:認證機制的選擇取決於你的架構,但無論選什麼,把 token 存在 localStorage 就是在跟攻擊者說「來拿啊」。

結論先講:JWT 配 Access Token + Refresh Token 是現代 API 認證的主流方案。但如果你把 Access Token 存在 localStorage,一個 XSS 漏洞就能讓攻擊者拿走你所有使用者的身份。

Session vs Token:先搞清楚自己適合哪種

這兩種認證機制的根本差異在於狀態存在哪裡

Session-based:狀態存在伺服器。使用者登入後,伺服器建立一個 session,把 session ID 塞進 cookie 發給瀏覽器。每次請求帶上 cookie,伺服器去查 session 確認身份。

  • 適合:傳統 server-rendered 應用
  • 問題:要擴展的話需要共享 session store(例如 Redis),不然使用者打到不同台機器就認不出來

Token-based:狀態存在客戶端。伺服器簽發一個 token,客戶端自己保管。每次請求帶上 token,伺服器驗簽就知道你是誰。

  • 適合:SPA、行動應用、微服務
  • 好處:天然支援水平擴展,不需要共享 session store

現代架構基本上都走 Token-based,所以接下來我們聚焦在 JWT。

JWT 的三段式結構

JWT 長這樣:xxxxx.yyyyy.zzzzz,三段以 . 分隔,都是 Base64 編碼:

  • Header:演算法跟 token 類型,例如 {"alg": "HS256", "typ": "JWT"}
  • Payload:攜帶的資料(Claims),例如使用者 ID、角色、過期時間
  • Signature:用密鑰對前兩段簽名,確保內容沒被篡改

注意:JWT 的 payload 是 Base64 編碼不是加密。任何人拿到 token 都能解出 payload 的內容。所以不要在 JWT 裡放密碼、信用卡號這類敏感資訊。

Access Token + Refresh Token:短刀配長劍

這是現在最主流的做法:

  • Access Token:有效期短(15 分鐘到 1 小時),用來存取 API 資源
  • Refresh Token:有效期長(7 天到 30 天),用來換發新的 Access Token

為什麼要這樣設計?因為 Access Token 會頻繁在網路上傳輸,洩漏風險高。有效期短,即使被偷了影響也有限。而 Refresh Token 只在換發 Access Token 時使用,傳輸頻率低,相對安全。

關於儲存位置,我認真的

很多教學文章教你把 JWT 存在 localStorage。拜託不要。

localStorage 在 XSS 攻擊面前完全沒有防禦力。攻擊者只要找到一個 XSS 漏洞,一行 localStorage.getItem('token') 就拿走你的 token 了。

正確的做法:

  • Access Token:存在 JavaScript 記憶體中(變數)。頁面重整會消失?沒關係,用 Refresh Token 重新拿一個
  • Refresh Token:存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中。JavaScript 碰不到,XSS 也偷不走

這樣做會比較麻煩嗎?會。但安全性不是你可以偷懶的地方。

OAuth 2.0:讓別人幫你做認證

OAuth 2.0 不是認證協議(那是 OpenID Connect),它是授權框架。但實務上大家都拿它來做「用 Google / GitHub / Facebook 登入」。

兩個最常用的流程:

Authorization Code Flow(有後端的 Web 應用):

  1. 使用者點「用 Google 登入」
  2. 跳轉到 Google 授權頁
  3. 使用者同意
  4. Google 把 authorization code 發到你的後端
  5. 後端用 code 跟 Google 換 access token
  6. 後端用 access token 拿使用者資訊

為什麼要繞這一圈?因為 access token 只在後端跟 Google 之間傳遞,前端完全碰不到。安全。

Client Credentials Flow(服務對服務): 服務 A 用 Client ID + Secret 直接跟授權伺服器換 token,然後拿 token 打服務 B。適合微服務之間的通訊。

API Key:最簡單但最弱

API Key 就是一個字串,放在 header 裡:

GET /api/data HTTP/1.1
X-API-Key: sk_live_abc123def456

優點:實作超簡單。缺點:沒辦法攜帶使用者身份、洩漏了就全完了。

適合的場景:第三方整合、內部微服務、公開但需要計量的 API。不適合用在需要區分使用者身份的場景。

速率限制:你的 API 不是自助餐

沒有速率限制的 API 就像不設限的自助餐——遲早有人把東西搬光。

常見的算法:

  • Fixed Window:固定時間窗口(每分鐘 100 次)
  • Sliding Window:滑動窗口,更平滑
  • Token Bucket:令牌桶,允許短暫的突發流量

不管用哪種,回應 header 要告訴 client 目前的額度狀態:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1694780060

登入端點要設更嚴格的限制(例如每 15 分鐘最多 5 次),不然暴力破解密碼就是送分題。

這篇的重點回顧

認證機制選 JWT + Access Token / Refresh Token 是主流。Access Token 存記憶體、Refresh Token 存 HttpOnly Cookie、永遠不要碰 localStorage。OAuth 2.0 讓別人幫你做認證,API Key 只用在服務對服務。速率限制不是選配是標配。

下一篇看完整的程式碼範例——Express Router、JWT Middleware、Rate Limiter。

系列文章:

延伸閱讀:

「安全性就像鎖門——你不會因為這條街治安好就不鎖,但你會因為鎖了而在出事的時候慶幸。」