iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

Day11_API 不是人人都可以呼叫的,來談談什麼是 JWT

前言

Day 10 把 Vue 與 ASP.NET Core API 拆成兩個應用程式。這樣一來,除了網頁,手機 App 或其他系統也能透過 HTTP 呼叫後端。不過,呼叫得到 API,不代表所有功能都能使用。查看自己的專案、修改他人的 Task,以及調整帳號角色,本來就應該有不同的權限。

所以,後端得先回答兩個問題:這個請求是誰送的?他能不能做這件事?前者是身分驗證(Authentication),後者是授權(Authorization)。JWT 常出現在這段流程中,卻不等於整套登入與權限機制。

JWT 是什麼?

JWT 的全名是 JSON Web Token。按照 RFC 7519 的定義,它是一種簡短、適合在 URL-safe 環境中傳遞的 Claims 表示格式。Claim 就是發行者對某個主體的一項聲明,例如「使用者編號是 123」或「系統角色是 User」。

使用者登入成功後,伺服器可以簽發一顆 JWT Access Token。之後前端每次呼叫受保護的 API,都要把它放在 HTTP Header 一起送出:

GET /api/v1/projects HTTP/1.1
Host: api.example.com
Authorization: Bearer <access-token>

Bearer 的意思很直接:誰拿到這顆 Token,誰就能拿它嘗試呼叫 API。因此 Token 必須透過 HTTPS 傳輸,也不能寫進 Log、URL 或錯誤訊息。

三段式結構裡面放了什麼?

本文談的是 API 常用的簽章型 JWT。它的外觀是一串由兩個句點分隔的文字,因此可以分成三段。下面為了方便閱讀才斷行,實際傳輸時會放在同一行:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJMb3VpcyIsInJvbGUiOiJVc2VyIn0
.
<signature>
Header.Payload.Signature
  • Header 記錄 Token 類型與簽章演算法,例如 typ: JWTalg: RS256
  • Payload 放置 Claims,例如使用者編號、角色、發行者與到期時間。
  • Signature 讓 API 能檢查 Header 與 Payload 是否被竄改,並確認 Token 是由持有簽章金鑰的一方發行。

這裡有一個很容易搞混的地方:Header 與 Payload 通常只是 Base64url 編碼,不是加密。任何拿到 Token 的人都能解碼內容,所以 Payload 不應放密碼、身分證字號、信用卡號或其他敏感資料。簽章用來保護完整性與確認來源,不會把內容變成只有伺服器才看得懂的密文。

常見 Claims

Claim 用途
iss Issuer,識別 Token 發行者
sub Subject,通常放使用者的穩定識別碼
aud Audience,指定這顆 Token 要給哪個 API 使用
exp Expiration Time,到期後不再接受
nbf Not Before,在指定時間之前不可使用
iat Issued At,記錄簽發時間
jti JWT ID,識別單一 Token

表格裡的 Claim 很常見,但不是每顆 JWT 都必須全部包含;應用程式還是要自行定義必填項目。我們也可以加入 rolepermission 這類自訂 Claim。不過,前端解碼後看到 role: Admin,只能用來調整畫面,不能當成授權結果。真正的 Token 驗證與權限檢查,仍然要由後端完成。

後端不能只把 Token 解碼

把 Token 解碼,只能讀到裡面的內容,還不能證明它可信。收到 Access Token 後,API 還要檢查:

  1. 簽章是否正確,而且 alg 必須在伺服器明確允許的演算法清單內。
  2. iss 是否為預期的發行者,aud 是否包含目前的 API。
  3. exp 是否已過期,nbf 是否已到可使用時間。
  4. 帳號是否仍可使用,及本次操作所需的角色、功能與資源範圍。

前三項用來確認 Token 的來源、用途與有效期,最後一項則回到當下的業務狀態。就算 JWT 有效,也不代表使用者能存取所有資源。例如一般 User 雖然已經登入,還是不能編輯自己未參與專案的 Task。

這兩種失敗情況在 HTTP 裡也有不同的表達方式。缺少 Token、簽章錯誤或 Token 過期時,通常回傳 401 Unauthorized;身分已確認但權限不足,則回傳 403 Forbidden。Day 12 談 API 契約時,會再看到這兩種狀態碼。

Access Token 為什麼還要搭配 Refresh Token?

Access Token 一旦外洩,攻擊者在它到期前都能冒用持有者的身分呼叫 API。縮短有效期可以減少受影響的時間,但如果 Token 一過期就要重新登入,使用起來又很麻煩。因此,實務上常會搭配壽命較長、受到更嚴格保護的 Refresh Token,用它換取新的 Access Token。

Refresh Token 不一定是 JWT。它可以是一串隨機文字,從外觀看不出任何資訊,並由伺服器保存對應狀態。這麼做後,系統就能實作以下兩個機制:

  • Rotation:每次更新成功就發出新的 Refresh Token,舊的立即失效。
  • Revocation:登出、帳號停用、密碼或權限發生重要變更時,伺服器可以主動撤銷尚未過期的 Refresh Token。

如果已經用過的 Refresh Token 又出現,它可能曾被複製。這時伺服器應拒絕請求,撤銷同一系列的 Token,再要求使用者重新登入。這是 OAuth 2.0 Security Best Current Practice 建議的 Refresh Token 重放偵測做法之一。

這個專案如何保存 Token?

ProjectManagementWeb 將兩種 Token 分開保存:

類型 有效期 前端保存位置 用途
RSA JWT Access Token 預設 15 分鐘 Vue 應用程式的記憶體 透過 Authorization: Bearer 呼叫業務 API
隨機 Refresh Token 預設 7 天 PMW-REFRESH HttpOnly Cookie 供 Refresh 與 Logout 端點更新或撤銷 Token

可以把 Access Token 想成一張有效期只有 15 分鐘的公司識別證。後端發證時,會在裡面寫上使用者編號 sub、Token 編號 jti、名稱 name、角色 role 與證件版本 token_version。每一筆 permission 就像這張證可以開啟的門,沒有對應權限,就不能進入。證件上還會註明由誰簽發、要交給哪個系統,以及何時過期。

API 收到這張識別證後,不會看到名字就直接放行。它會先檢查 RSA 簽章是不是真的,再核對發行者、使用對象與有效期。這些都通過後,還要確認帳號目前仍是啟用狀態,而且 token_version 與資料庫一致。

Vue 拿到 Access Token 後,只會暫時放在應用程式的記憶體,就像把識別證拿在手上,不會另外抄一份放進 Local Storage。這麼做雖然不能解決所有網頁攻擊,但可以減少 Token 被長期保留與竊取的風險。

Refresh Token 則像「換新證的憑據」。後端會產生一組 64 位元組的高熵隨機值,再把原文裝進名為 PMW-REFRESH 的 Cookie。JavaScript 因為 HttpOnly 讀不到它,資料庫也只保留像指紋一樣的 SHA-256 hash。這顆 Cookie 的 Path 是 /api/v1/auth,設定了 SameSite=Lax;使用 HTTPS 時,還會加上 Secure

不過,憑據放在 HttpOnly Cookie 裡,不代表就可以忽略 CSRF。瀏覽器還是會自動帶上這顆 Cookie,所以系統另外準備了一組核對碼。前端會先呼叫 GET /api/v1/security/csrf-token 取得它,發送註冊、登入、Refresh、登出與 Email 驗證相關的 POST 請求時,再將它放進 X-CSRF-TOKEN。至於一般業務 API,用的仍然是 Bearer Access Token,不會把 Refresh Token Cookie 當成身分憑證。

如果業務 API 回傳 401,就像服務人員說「這張證不能用了」。前端會拿 Refresh Token 去換一張新的 Access Token,成功後再把原本的請求重送一次。為了避免多個請求一起擠到櫃檯前換證,前端同一時間只會發出一個 Refresh 請求。否則多次 Rotation 互相影響,剛換到的 Token 也可能立刻被撤銷。

token_version 則像識別證的版次。當帳號被停用或系統角色變更,後端會把版次加一,同時撤銷這個帳號尚未失效的 Refresh Token。使用者如果再拿舊 Access Token 呼叫 API,就會因為證件版本與資料庫對不上而被拒絕。這會多一次資料庫查詢,卻不必等到 15 分鐘後,才能阻止舊 Token 繼續使用。JWT 可以做無狀態驗證,但這個專案更在意帳號異動後,舊證件能不能及時失效。

小結

JWT 處理的是 Claims 如何以簡短格式傳遞,並透過簽章驗證完整性與來源。它不會加密 Payload,也不會自動處理權限、撤銷、Token 儲存或 CSRF。這些邊界還是要由應用程式自己設計;只是能把 Token 解碼,還遠遠不夠。

身分與權限的邊界釐清後,下一步是把「要呼叫哪個資源」、「使用什麼 HTTP 方法」、「成功或失敗會收到什麼」說清楚。Day 12 會從這裡接著談 RESTful API 與 API 契約。

參考資料


上一篇
Day10_我的架構不只是給網頁用,來談談什麼是前、後端分離
下一篇
Day12_前後端分離的溝通方式,RESTful API與契約
系列文
Codex的規格驅動開發 :30 天打造 .NET 內部專案管理系統14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言