前言
於 JWT 的介紹,會說明基本規範、使用範例與實務管理方式。這個主題會分成兩篇文章說明,分別是:
- 本篇 JWT 基礎概念
- 第二篇,各種驗證的 token 差異,以及 JWT 實際使用上的管理方式
第二篇預計會包含:
- access token
- refresh token
- security stamp
- Token 撤銷與登入狀態管理
- 網路上關於 JWT 的批評本質上無狀態 (Stateless) token 的共通問題
為何需要 JWT
在一個服務實作登入,或具備身分驗證功能時,系統需要一種方式,讓客戶端在後續請求中證明自己的身份,並讓伺服器能夠驗證該身份是否合法。
在早期常見的做法是使用 Session 的有狀態 (Stateful) 機制,也就是由伺服器端保存使用者的登入狀態。客戶端在登入之後伺服器會建立一個 session ID ,並傳給使用者端儲存在 Cookie 中。當使用者每次向伺服器發送請求時,瀏覽器都會帶上此 session ID ,然後伺服器再用此 session ID 查詢對應的登入狀態。
這種方式的優點是登入狀態、登出、撤銷與權限變更都可以由伺服器端集中控制。不過當服務逐漸擴張,系統拆分成多台伺服器,甚至演變成前後端分離或微服務架構時,就需要處理多台伺服器之間的 Session Storage 共享與同步問題,進而提高系統擴展與維運成本。
於是有人提出透過簽章驗證,將部分資料儲存於客戶端 Token 端的無狀態(Stateless)驗證方式。
在早期 JWT 尚未標準化前,有許多實作無狀態 (Stateless) 方式,早期也有許多系統使用自定義 Bearer Token、API key、自訂 Cookie-based Token,或由 OAuth/OIDC 系統簽發的各種 access token 格式。直到 RFC 7519 標準化 JWT 之後,JWT 才逐漸成為常見的 Token 格式之一。
JWT 並不是 Session 的全面替代方案,而是在特定架構需求下更適合的選擇。
JWT 基本概念
JWT 全名是 JSON Web Token
JWT 並非單一文件,而是建立在一系列由 IETF 制定的 RFC 規範之上,主要包含:
RFC 7519 JSON Web Token(JWT): JWT 本體規範
- JWT 的三段式結構 (Header.Payload.Signature)
- 標準欄位 (Registered Claims)
- Token 的語意與驗證邏輯
RFC 7515 JSON Web Signature (JWS): JWT 要如何簽章與驗證
常見演算法:
- HS256 (HMAC + shared secret)
- RS256 (RSA + private/public key) *業界常用
- ES256 (ECDSA)
RFC 7516 JSON Web Encryption (JWE)
不過業界很少對 JWT 進行加密,所以並不常用
- 可讓 payload 內容本身加密
- 實務上 JWE 使用相對少見
- 多數系統只做「簽名不加密」
JWT 三段式結構
在 RFC 7519 規範當中, JWT 本體由三段式結構所組成,分別是
Header.Payload.Signature 其真實樣態和結構會像是如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30在 JWT 結構當中以 . 將字串來分隔成 Header.Payload.Signature 三個部分。
前兩段 Header 與 Payload ,本質上只是 JSON 資料經過 Base64URL 編碼後的結果, 並不是加密資料 。
也就是說,只要進行解碼,就能看到其中的原始內容。因此 JWT 不適合存放敏感資訊,例如密碼、信用卡資訊或個資明文。
重要的是第三段簽章,伺服器在簽發 JWT 時,會使用 Key (Secret Key 或 Private Key) ,搭配演算法,對前兩段內容產生簽章。
當伺服器收到 JWT 時,就能重新驗證這段簽章是否正確。如果有人私自修改 Payload 內容,簽章就會失效,JWT 也會被判定為無效。
如果想實際查看內容,可以到 jwt.io 線上解析 JWT。
JWT 的 Payload Claims 多數不是每個 Token 都必填,而是依照應用場景選用。不過在 JWS 簽章型 JWT 中,Header 的 alg 是必要欄位,否則驗證方無法知道該使用哪種演算法處理 Signature。
JWT 第一段 Header 的作用:告訴伺服器要如何驗證該 Token
JWT Header 主要提供驗證流程所需的 metadata,例如 Token 宣告使用的演算法,以及可協助伺服器選擇驗證金鑰的 key id。不過 Header 在驗證完成前不能被完全信任,伺服器仍應使用自身設定的允許演算法與可信任金鑰來源進行驗證。
Header 不承載身分、不承載權限、不承載狀態,這部分資料主要是放在 Payload 的 Claims 當中。不過要注意, Header 在 Token 尚未驗證前也不能被完全信任,它主要是提供驗證流程所需的 metadata。
可以把 JWT Payload 理解成這張通行證的內容描述:它在說明這個 Token 代表誰、用途是什麼、何時失效,以及驗證方可以如何理解它。
JWT Header 常見欄位包含 typ、alg、kid,以及部分進階情境才會使用的 cty、jku、x5u、x5c、x5t 等,詳述如下:
- typ: Type, 標示整個 Token 的媒體類型。一般 JWT 常見值是
JWT。實務上很多函式庫不依賴這個欄位驗證 Token,因此可加可不加,但加上可以提升可讀性。 - alg: Algorithm, 在 JWS 簽章型 JWT 中表示簽章或 MAC 使用的演算法,例如
HS256、RS256、ES256。在 JWS 中這是必填欄位,沒有alg通常就無法知道該用哪種演算法驗證 Signature。實務上不要直接信任 Token 自己帶的alg,而是應該由伺服器端設定允許的演算法清單,避免演算法混淆或錯誤接受none類型的風險。 - kid: Key ID, 數值可以自定,如:
key-2025-01,通常表示該簽章是由哪個 key 簽發的,如果系統有多把 key 處理不同的 JWT 簽發,或是有做定期 Key rotation ,在實務上強烈建議要加,不然在驗證時,就無法知道要用哪把key對 Signature 進行驗證。
進階但「有條件才用」的 Header 欄位
- cty: Content Type, 表示 payload 的實際內容型別, 常見於 Nested JWT(JWT 包 JWT) 和 JWE + JWS 混用。例如一個 JWT 被簽章或加密後又包進另一個 JWT。一般 Web / API 登入驗證幾乎用不到。
- jku / x5u: Key URL / Certificate URL, 告訴驗證方「去哪抓 key」,例如
{ "jku": "https://example.com/jwks.json" }。⚠️ 極度不建議亂用,容易變成 SSRF / key injection 攻擊面,業界通常是 key endpoint 是固定設定,不會使用該欄位資訊。 - x5c: X.509 憑證鏈,憑證鏈驗證。一般 JWT API 不會用。
- x5t / x5t#S256: X.509 憑證指紋,分別對應 SHA-1 與 SHA-256。一般 Web API JWT 驗證很少需要直接使用。
JWT 第二段 Payload 的作用:描述這個 Token 代表誰、用途是什麼、何時有效等資訊
JWT Payload 是存放聲明 (Claims) 」的地方,也就是描述這個 Token 代表誰、用途是什麼、何時有效等資訊。
JWT 的 Payload 不是加密內容 ,只是 Base64URL 編碼,任何拿到 Token 的人,都可以把 Header 和 Payload 解碼成可讀的 JSON ,所以不要放密碼、敏感個資。
實務上, Payload 通常只放最低限度、足以識別請求來源的資料,例如使用者 ID 、 Token 用途、有效期限等。至於更詳細的使用者資料、權限資料或狀態資訊,通常會在後端 Middleware 階段,根據 Token 中的識別資訊進一步查詢資料庫,然後補到 Request 物件中,供後續的 Controller 或 Service 使用。
因為 payload 欄位定義相當自由,依照標準與業界使用習慣可以分為以下三大類:
- RFC 7519 註冊欄位 (Registered Claims): JWT 標準中已註冊的 Claim 名稱。
- 公開自定義欄位 (Public Claims): 公開自定義欄位,適合需要跨系統共用的欄位,但要避免命名衝突。
- 私有欄位 (Private Claims): 系統內部或合作雙方自行約定的欄位。
這裡就對這三大類的欄位分別說明:
RFC 7519 註冊欄位 (Registered Claims)
Registered Claims 是 JWT 標準中預先定義好的 Claim 名稱,目的是讓不同系統之間可以用一致的語意理解 Token。
這些欄位不是每個 JWT 都必須使用,而是可以依照服務需求自由選用。
常見的 Registered Claims 包含以下幾種:
- iss: Issuer, 這個 JWT 是誰簽發的,常見值可能放 URL ,服務名稱或系統識別名稱。驗證方可以用它來確認 Token 是否來自可信任的發行者。
- sub: Subject, 主體,這個 JWT 所有者資訊,通常為帳號 ID 、 服務 ID 、 Client ID 等,它通常用來表示「這個 Token 代表誰」。
- aud: Audience, 接收者,用於說明這個 JWT 是要給哪個服務使用的,可以給後端檢查是否符合目前的 API 或服務,避免某個服務簽發的 Token 被拿去呼叫不該使用的服務。
- exp: Expiration time, 到期時間,通常格式是 UTC Unix timestamp 的「秒」,如
1777793400。驗證方可以用目前時間與 exp 比較,判斷這個 Token 是否已經過期。 - nbf: Not before, 在這個時間點之前不能使用,它常用於預先簽發 Token,但稍後才啟用。通常格式為 UTC Unix timestamp 。
- iat: Issued at, 簽發時間,通常格式為 UTC Unix timestamp ,不一定會直接用於授權判斷,但常見於記錄 Token 的簽發時間、 Debug、稽核、限制 Token 年齡,或搭配其他安全機制使用。
- jti: JWT ID, 表示這個 Token 的唯一識別碼,通常會使用
UUIDNano IDCrypto Random Bytes等方式產生不容易猜測的唯一不重複隨機字串,以確保每次產生的 JWT 都有唯一識別碼,不建議使用有規則流水號,因為可能被外部推測出系統內部資訊。如果系統需要支援 Token 撤銷、登出失效、資安追蹤、一次性 Token,或 refresh token 管理,就很適合加入jti。但對於短效 access token,是否加入jti可以依照系統需求取捨。
JWT Registered Claims 可以依照服務需求自由選用,不需要每個欄位都放。實務上最常見的基本組合通常會包含 sub、exp、iat,如果系統有多個服務或多個 Token 發行者,也常會搭配 iss 和 aud 一起驗證。
公開自定義欄位 (Public Claims) 和 私有欄位 (Private Claims) 因為在實務上大家多半會統稱為 自定義欄位(Custom Claims) ,因此這部分會合併說明。
嚴格意義上的公開自定義欄位 (Public Claims) ,是指有在 IANA JSON Web Token Claims Registry 註冊的欄位名稱,或是使用具備 Collision-Resistant Name 特性的 Claim Name。
Public Claims 的「Public」不是指內容被公開,而是指欄位名稱具有公開定義或避免命名衝突的特性。
例如在 OpenID Connect 場景中,常會看到一些用來描述使用者基本資料的 Claims :
{
"email": "[email protected]",
"name": "Tom",
"picture": "https://example.com/avatar.png"
}
若要避免與其他系統的欄位名稱發生命名衝突,也可以使用 URI 形式作為 Claim Name ,例如:
{
"https://example.com/claims/role": "admin",
"https://example.com/claims/tenant_id": "tenant_001"
}
這種使用嚴格意義上的公開自定義欄位 (Public Claims) 通常只見於 OAuth / OpenID Connect 場景中,或是需要設計跨系統、開放第三方整合的 Token 場景。
私有欄位 (Private Claims) 通常是公司企業內部自行定義,僅供內部系統理解與使用,欄位名稱為企業內部自行定義,不需要跟外部系統交流,例如:
{
"tenantId": "abc-company",
"securityStamp": "v3",
"deviceId": "iphone-001"
}
不過在一般後端專案的實務討論中,開發者通常不會嚴格區分 Public Claims 與 Private Claims,而是會將這些非 JWT 官方標準欄位統稱為 自定義欄位(Custom Claims)。
JWT 第三段 Signature :驗證 Token 是否被竄改
JWT 的第三段 Signature 是簽章,用來驗證 Header 與 Payload 是否曾被竄改。它並不是用來加密 JWT,而是透過 Header 指定的演算法與伺服器端持有的密鑰,根據前兩段內容計算出一段簽章值。
可以將概念理解為伺服器在簽發 JWT 時,會將 Header 和 Payload 的 JSON 資料進行 base64UrlEncode 編碼,然後再用 . 連接起來,概念大概像是這樣:
base64UrlEncode(header) + "." + base64UrlEncode(payload)結果會像是這樣的字串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0接著伺服器會使用密鑰與指定演算法,對這段字串進行簽章運算。以常見的 HMAC SHA-256 為例,概念上可以理解成:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
最後會產生類似這樣的 Signature :
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30當伺服器收到 JWT 時,會使用相同的演算法與密鑰重新計算 Signature,並與 Token 第三段進行比對。如果結果一致,表示 Token 內容未被修改;如果不一致,則代表 Token 可能被竄改,應拒絕該請求。
因此, Signature 保護的是資料完整性與簽發可信度,不是資料機密性。Payload 仍然只是 Base64URL 編碼,不能放敏感資料。
參考
AI 詢問資料


















