使用 JWT 的反思
重新檢視在專案中選擇 JWT 作為認證機制,是否合適,以及付了哪些代價。
This page hasn't been translated yet — you're viewing the Chinese version.
認證機制
認證機制是為了讓使用者登入之後,系統能記住「這個人是誰、有什麼權限」,後續的請求就不必重複驗證,減少系統負擔與延遲。
當初在做個人專案的系統設計時,正好是 microservices 風氣盛行,因此我在系統設計偏好:
- Resiliency:服務掛掉後能快速重啟,且重啟後仍維持資料的一致性。
- Extensibility:微服務架構下,能更輕易地擴展、也更容易跟其他服務對接。
這兩點的共同前提,都是希望服務盡可能是 Stateless 的。因此在 Auth 這一塊,我選擇了 JWT 而不是 Session。
Stateless(無狀態)是指每個請求都攜帶處理所需的完整資訊,伺服器不需要記住先前請求的任何狀態。
這篇文章的重點不是介紹 JWT 和 Session 是什麼,所以不會著墨太多細節。我想寫的是另外兩件事:選了 JWT 之後實際會遇到什麼,以及後來理解更多概念之後,回頭看當初這個選擇的反思。
JWT vs Session
先簡述兩者分別是什麼。
JWT(JSON Web Token) 是一種傳遞資訊的標準格式。它最大的特色是把資訊(稱為 claim)直接存放在 token 裡,並透過簽章(Signature)確保內容沒有被竄改。
JWT 反映的其實是 Token-based authentication 這個更大的概念,只是因為 JWT 是實務上最常見的實作,所以網路上的討論多半直接拿 JWT 來談,本篇也是如此。
JWT 簽發出來的 token 通常有兩種用途:
- Access Token:帶著使用者的權限資訊,用來決定「這個請求能不能做這件事」。
- ID Token:帶著使用者的身份資訊,用來回答「這個人是誰」。
延伸閱讀 — JWT 官方文件:jwt.io
Session 的設計則相反:資訊儲存在伺服器端(通常是 Cache),只給使用者一組 Session ID 當作鑰匙。後續每個請求,伺服器都拿這個 Session ID 回去比對,確認身份與權限。
JWT 的痛點
把資訊都塞進 token、讓伺服器不用負擔太多責任,這件事看起來很美好,但同時也衍生出一些需要留意的外部性問題。
1. JWT 是 encode,不是 encrypt
它的簽章機制只能確保資訊「不被竄改」,不能確保資訊「不被看到」。任何人——包含攻擊者——只要取得 JWT,就可以直接 decode 出裡面的資訊(又稱為 claim)。
應對方式:不要把機密資訊放進去(銀行卡資訊、密碼、個資等)。
2. 作為 Access Token 簽發出去的 JWT 無法被撤銷
如果 JWT 被竊取,伺服器沒有辦法讓它失效——因為伺服器從來沒有追蹤過自己簽發了哪些 JWT,自然也就無從識別、無從封鎖。
應對方式:把有效期設短(視情境而定,常見是 5~15 分鐘)。
要注意這比較像是控制 blast radius,也就是控制事情失控的程度,而不是真的解決問題——攻擊者在這段時間內,仍然能拿著這張 JWT 存取服務。
延伸討論:另一種做法是把已知外洩的 JWT 放進黑名單,但這等於犧牲掉 JWT 原本「stateless authentication」的優勢——你需要一個地方去記錄,並且在每次認證時多一步查詢。
相比之下,Session 在這方面的應對就容易得多:伺服器想讓一個 session 失效,把 Redis 裡那筆刪掉就結束了。
JWT 實作細節
1. 儲存位置
當初在做 side project 的時候,我是參考 OAuth 的流程,把 token 放進 Authorization header,讓 JS 能自己控制要不要帶上 token。所以一開始就沒有考慮 Cookie,只在 client storage 之間衡量:sessionStorage 還是 localStorage。
sessionStorage 會隨著分頁或瀏覽器關閉而清空,localStorage 除非被明確刪除否則會一直保留——為了不要讓使用者一直重登,我選了 localStorage。
但再深入研究,會發現這題其實眾說紛紜,論點大致落在通用性/便利性與安全性兩端:
- localStorage:容易使用與存取,但有 XSS(Cross-Site Scripting)風險——一旦頁面被注入惡意腳本,token 可以被整個讀走並帶到別處重放。
- Cookie:通用性較低,基本上只適用瀏覽器,對 server-to-server、IoT device 等相對不友善;而且因為瀏覽器會自動附帶,需要另外防範 CSRF 攻擊。
- In-memory(放在 JS 變數裡):Auth0 最推薦的方式。token 只存在記憶體中,所以 XSS 能偷到的時間窗口最小;代價是使用者一重新整理頁面 token 就沒了,必須搭配 refresh token 或 silent authentication 才能維持登入狀態。
這裡值得留意的是:這三個選項沒有一個是「完全安全」又「方便」的,屬於安全與複雜之間的權衡。
眾說紛紜的 StackOverflow 討論:stackoverflow.com/questions/48712923
Token Storage(Auth0 官方文件):auth0.com/docs/secure/security-guidance/data-security/token-storage
2. 有效期限 vs 使用者體驗
前面提到,為了安全性我們把 JWT 的有效期設得很短,假設是 15 分鐘。但這馬上換來一個新問題:使用者每 15 分鐘就要重新認證一次,這在現在的網站上顯然沒有人能忍受。
解決方式:Refresh Token。
Refresh token 的想法很直接:把「證明身分」和「維持登入」這兩件事拆開,用兩張效期完全不同的 token 各自負責。
- Access token(5~15 分鐘):每次 API 請求都帶上,效期極短,被偷了也很快失效。
- Refresh token(數天~數週):只用在一個地方——跟伺服器換一張新的 access token。
Access token 過期的時候,前端會主動拿 refresh token 去換一張新的,整個過程在背景完成,因此使用者不會有重新驗證的感覺。
值得一提的是,refresh token 本身通常不是 JWT,而是一組不帶任何資訊的隨機字串(opaque token)。因為它每次使用都要回伺服器驗證,本來就沒有「自帶資訊、免查詢」的需求。
由於 refresh token 效期長、被偷的風險更高,實務上會再多做兩件事:
-
傳遞上:放在
SameSite=Strict+HttpOnly的 cookie 裡。前者讓瀏覽器不會在跨站請求時附帶它,降低 CSRF 風險;後者讓 JavaScript 讀不到它,即使發生 XSS 也無法把 token 整個帶走。 -
儲存上:refresh token 會存在伺服器端(DB 或設有 TTL 的 Redis)。這樣系統才有能力主動讓它失效——使用者登出、偵測到異常、管理員強制下線,都需要一個地方去標記「這張作廢了」。
更嚴謹的做法還包含 Refresh Token Rotation:每次換發都同時汰換 refresh token,舊的立刻作廢;如果偵測到已作廢的 token 又被使用,就判定為遭到竊取,撤銷整個 token family。
寫到這裡會發現一件有點矛盾的事。當初選 JWT 的理由是 stateless,但要讓它兼顧安全性,我必須把有效期壓短;壓短之後需要 refresh token 來維持體驗;而 refresh token 為了驗證,又必須存在伺服器端。
所以就有我下一個章節的疑問。
那什麼時候才真的需要 JWT?
講了 JWT 那麼多複雜之處,它之所以還是被設計出來、還是被廣泛使用,是因為有些場景 session 真的做不到:
1. 共享 session 本身有困難的情況
- 服務數量多:由上游的 Auth Server 統一簽發 JWT,下游服務直接驗證 token 就好,不必讓每個服務都去讀取同一份認證資料或快取。
- OAuth 第三方授權:分屬不同平台、不同立場的服務之間,不可能互相分享 session。加上瀏覽器對第三方 cookie 的限制越來越嚴,走 Authorization header 的 JWT 剛好可以繞過這個限制。
2. 流量大到 session 查詢真的變成瓶頸
這一條要特別小心。Redis 這類 cache 在承載 session 上,往往比想像中更經得起流量,所以拿「效能」當理由的時候,最好要有實際的壓測數據或線上紀錄佐證,而不是預設它遲早會成為瓶頸。
OIDC、OAuth 這些框架的基礎都還是 token-based 認證,只要你的系統需要串 Google、Facebook 等第三方登入,就一定會遇到它。
反過來,如果系統是單一後端服務、前後端同網域、同時在線在幾萬以內,session + cookie 幾乎都是更合理的選擇:伺服器對登入狀態有更高的控制權(ex. 撤銷、登出),也避免了整套 token 生命週期的實作與維護成本。
如果讓我重新選擇認證機制?
回應到我最開頭,在 side project 選擇 tech stack 中,當初單純為了追求 Stateless 而去使用 JWT ,其實沒那個必要。
為了省下 session 的維護與查詢,我多煩惱 token 衍生的 3 件事情:
- token 該存哪裡的兩難
- 有效期與使用者體驗的拉扯
- 以及無法有效注銷 token
而 refresh token 又造就了系統 session stateful 的必要。
因此,如果讓我重新選擇認證機制
- 若是自己個人的 project,我會優先考慮用 cookie + session
- 先具備 Cookie 設置的相關知識
- 使用 Redis 做 session 的管理
- 善用與框架提供的套件,而不是從頭開發。把時間投入在專案的核心價值上。
- 若是面對大型、production 的專案
- 若還未建立認證系統,會選擇使用 Supabase、AWS Cognito 等成熟的託管驗證服務,節省時間並降低自行實作安全機制的風險。
- 若已有既有認證系統,則審視目前架構如何處理異常事件,例如 token 洩漏、撤銷、權限變更、多裝置登入管理等,並評估改善空間。
總結
JWT、token-based authentication、OAuth 這些概念與名詞,在網路上的討論常常混在一起講,對剛接觸的 Engineer 來說特別容易搞混,並很難建構完整的認知。
要完整地設計與實作一套 token-based 認證,需要考量的東西比想像中多很多:短效 access token、長效 refresh token、token 的儲放位置、rotation。
我想這就是 Auth0、Supabase Auth 這類服務興起的原因:它們幫你處理掉上面這些安全性認證的細節,讓團隊能專注在商業邏輯上。對於已經有一定規模、客戶資料出事會造成實質風險的服務來說,用現成的方案通常比從頭實作更安全也更省力。
然而,就算用的是現成的 solution,理解 JWT、session、token-based 認證的原理仍然有其價值。它決定了:
- 能不能正確讀懂 IDaaS 的官方文件,出問題時能不能更快定位並 debug。
- 能不能看懂論壇上工程師們在辯論什麼、trade-off 究竟為何。
延伸 Topic
- OAuth 2.0
- Cookie
- XSS、CSRF 攻擊與防範
參考資料
- Web Dev Simplified,Cookies vs Local Storage vs Session Storage
- JWT 官方介紹
- Auth0,Token Storage
- Auth0,Refresh Token Rotation
- Kalan,再談 JWT 與 Session Cookie
- StackOverflow,Where to store a JWT token properly and safely
← Back to blog