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 應用):
- 使用者點「用 Google 登入」
- 跳轉到 Google 授權頁
- 使用者同意
- Google 把 authorization code 發到你的後端
- 後端用 code 跟 Google 換 access token
- 後端用 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。
系列文章:
- API 設計(一):RESTful 基礎
- 你在這裡 → API 設計(二):認證機制
- API 設計(三):實戰程式碼範例
- API 設計(四):API 文件化與常見陷阱
延伸閱讀:
「安全性就像鎖門——你不會因為這條街治安好就不鎖,但你會因為鎖了而在出事的時候慶幸。」