在上一篇文章中,我們介紹了 JWT 的運作原理:為什麼需要 JWT、簽名機制如何防止偽造、以及完整的登入流程。
這篇文章,我們要深入探討 JWT 的內部結構。
回顧:JWT 的三個部分
JWT 是一個用「.」分隔的字串,由三個部分組成:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiam9obiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
拆開來看:
- Header(標頭):描述這個 Token 的「元資料」
- Payload(內容):實際要傳遞的「資料」
- Signature(簽名):用來驗證 Token 有沒有被竄改
接下來,我們逐一深入介紹。
Header(標頭)
Header 用來描述這個 JWT 的「元資料」,也就是「這個 Token 是怎麼產生的」。
Header 的內容
一個標準的 Header 長這樣:
{
"alg": "HS256",
"typ": "JWT"
}
alg(Algorithm)
alg 指的是「簽名演算法」,告訴伺服器:「這個 JWT 的簽名是用什麼演算法計算出來的」。
什麼是簽名演算法?你可以把它想成一套「計算公式」。把資料和密鑰丟進去,它會算出一個結果(簽名)。不同的演算法,就是不同的計算公式,算出來的結果也會不一樣。
常見的演算法有:
| 演算法 | 全名 | 類型 |
|---|---|---|
| HS256 | HMAC-SHA256 | 對稱式 |
| HS384 | HMAC-SHA384 | 對稱式 |
| HS512 | HMAC-SHA512 | 對稱式 |
| RS256 | RSA-SHA256 | 非對稱式 |
| RS384 | RSA-SHA384 | 非對稱式 |
| RS512 | RSA-SHA512 | 非對稱式 |
| ES256 | ECDSA-SHA256 | 非對稱式 |
關於對稱式和非對稱式的差別,我們後面會詳細說明。
typ(Type)
typ 指的是「Token 類型」,對於 JWT 來說,這個值就是 JWT。
你可能會想:「這不是廢話嗎?JWT 的類型當然是 JWT。」
沒錯,這個欄位看起來有點多餘。它存在的原因是:JWT 的格式(Header.Payload.Signature)也被其他標準採用,例如 JWS(JSON Web Signature)、JWE(JSON Web Encryption)等。typ 欄位可以幫助接收方快速判斷這是哪一種 Token。
但在實務上,這個欄位是可選的,大部分情況下看到的就是 JWT,不寫也沒關係。
Header 的編碼
Header 是一個 JSON 物件,會經過 Base64Url 編碼,變成 JWT 的第一段。
什麼是 Base64?它是一種把資料轉換成「純文字」的編碼方式。因為 JSON 裡面可能有特殊字元(像是 {、"、:),直接放在網址或 HTTP Header 裡可能會出問題,所以先用 Base64 轉換成只有英文字母、數字和少數符號的字串,方便傳輸。
Base64Url 是 Base64 的變體,把 + 換成 -、/ 換成 _,讓它可以安全地放在網址裡。
原始 JSON:{"alg":"HS256","typ":"JWT"}
↓ Base64Url 編碼
編碼結果:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload(內容)
Payload 是 JWT 的「主體」,存放實際要傳遞的資料。
Payload 的內容
一個 Payload 可能長這樣:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}Payload 裡的每一個欄位,我們稱為 Claim(聲明)。
標準 Claims
JWT 規範定義了一些「標準 Claims」,這些是大家約定俗成的欄位名稱:
| Claim | 全名 | 說明 |
|---|---|---|
iss | Issuer | 簽發者,誰簽發了這個 JWT |
sub | Subject | 主體,這個 JWT 是關於誰的 |
aud | Audience | 接收者,這個 JWT 是給誰用的 |
exp | Expiration Time | 過期時間,Unix 時間戳 |
nbf | Not Before | 生效時間,在此之前 JWT 無效 |
iat | Issued At | 簽發時間,JWT 是什麼時候簽發的 |
jti | JWT ID | JWT 的唯一識別碼 |
exp(過期時間)
這是最常用的 Claim。exp 是一個 Unix 時間戳,表示這個 JWT 什麼時候會過期。
{
"exp": 1702900000
}伺服器驗證 JWT 時,會檢查 exp 是否已經超過現在的時間。如果超過了,就拒絕這個 JWT。
iat(簽發時間)
iat 記錄這個 JWT 是什麼時候簽發的,可以用來計算 JWT 的「年齡」。
{
"iat": 1702896400
}
自訂 Claims
除了標準 Claims,你也可以放自己的資料:
{
"user_id": 123,
"username": "john",
"role": "admin",
"permissions": ["read", "write", "delete"]
}
這些自訂的欄位,可以放任何你需要的資料。
Payload 的編碼
跟 Header 一樣,Payload 也是經過 Base64Url 編碼:
原始 JSON:{"user_id":123,"username":"john"}
↓ Base64Url 編碼
編碼結果:eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiam9obiJ9
⚠️ 重要:Payload 不能放機密資訊
這是 JWT 最重要的安全觀念,請務必記住:
Payload 裡絕對不能放機密資訊!
為什麼?因為 Base64 是「編碼」,不是「加密」。
編碼 vs 加密
| 編碼(Encoding) | 加密(Encryption) | |
|---|---|---|
| 目的 | 轉換格式,方便傳輸 | 保護資料,防止被看到 |
| 可逆性 | 任何人都能還原 | 只有有金鑰的人能還原 |
| 例子 | Base64、URL Encoding | AES、RSA |
Base64 只是把資料轉換成另一種格式,任何人都可以輕易解碼回來。
實際示範
假設有人拿到了你的 JWT:
eyJ1c2VyX2lkIjoxMjMsInBhc3N3b3JkIjoibXlzZWNyZXQxMjMifQ他只需要做 Base64 解碼:
atob('eyJ1c2VyX2lkIjoxMjMsInBhc3N3b3JkIjoibXlzZWNyZXQxMjMifQ')
// 結果:{"user_id":123,"password":"mysecret123"}密碼就這樣被看光了!
不能放的資料
- ❌ 密碼
- ❌ 信用卡號
- ❌ 身分證字號
- ❌ 任何機密資訊
可以放的資料
- ✅ 使用者 ID
- ✅ 使用者名稱
- ✅ 角色(admin、member)
- ✅ 權限列表
- ✅ 過期時間
簡單來說:放你不介意被別人看到的資料。
線上解碼工具
你可以到 jwt.io 這個網站,把任何 JWT 貼上去,它會直接顯示 Header 和 Payload 的內容。
這也證明了:JWT 的內容對任何人都是「透明」的。
Signature(簽名)
Signature 是 JWT 的「防偽機制」,用來驗證 Token 有沒有被竄改。
簽名的計算方式
簽名是這樣計算出來的:
Signature = 演算法(
Base64Url(Header) + "." + Base64Url(Payload),
Secret
)以 HS256 為例:
Signature = HMAC-SHA256(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjN9",
"my-secret-key"
)為什麼簽名能防止竄改?
因為簽名的計算需要 Secret(密鑰),而這個密鑰只有伺服器知道。
如果有人想要竄改 Payload(例如把 user_id 從 123 改成 456):
- 他改了 Payload
- 但他不知道 Secret,算不出正確的新簽名
- 他只能帶著舊的簽名
- 伺服器重新計算簽名,發現對不上
- 驗證失敗,拒絕請求
sequenceDiagram
participant A as 攻擊者
participant S as 伺服器
Note over A: 修改 Payload,但算不出新簽名
A->>S: 發送竄改的 JWT(帶著舊簽名)
Note over S: 重新計算簽名,發現不一致
S->>A: ❌ 401 Unauthorized
簽名演算法:對稱式 vs 非對稱式
JWT 支援兩種類型的簽名演算法。
對稱式演算法(HMAC)
對稱式演算法使用同一把密鑰來簽名和驗證。
簽名時:把「資料」和「Secret」一起做雜湊運算,產生簽名。
驗證時:把「資料」和「Secret」重新做雜湊運算,產生新的簽名,然後比對兩個簽名是否一致。
flowchart LR
subgraph 簽名過程
D1[資料] --> H1[雜湊]
S1[Secret] --> H1
H1 --> SIG[簽名]
end
subgraph 驗證過程
D2[資料] --> H2[雜湊]
S2[Secret] --> H2
H2 --> SIG2[新簽名]
SIG2 --> C[比對]
SIG --> C
end
因為雜湊是單向的、不可逆的,所以簽名和驗證都必須用同一把 Secret 才能算出一樣的結果。
特點:
- 簽名和驗證都用同一把 Secret
- 只有雜湊,沒有加密
- 速度快
- 適合單一服務或內部系統
常見演算法:HS256、HS384、HS512
非對稱式演算法(RSA、ECDSA)
對稱式有一個問題:每個需要驗證 JWT 的服務,都必須知道 Secret。
假設你有 10 個服務都需要驗證 JWT,那這 10 個服務都要知道 Secret。知道 Secret 的服務越多,Secret 外洩的風險就越高。而且一旦 Secret 外洩,攻擊者就能自己產生 JWT,偽造任何人的身份。
非對稱式演算法就是為了解決這個問題。
它使用兩把不同的金鑰:
- Private Key(私鑰):用來產生 JWT 的簽名。只有「認證伺服器」知道,絕對不能外洩。
- Public Key(公鑰):用來驗證 JWT。可以公開給任何需要驗證的服務。
這兩把金鑰是「配對」的。這是什麼意思?
當你要使用非對稱式演算法時,會先用程式產生一對金鑰。這個產生過程用了特殊的數學公式,讓 Private Key 和 Public Key 之間有一種數學上的對應關係。
跟對稱式不同,非對稱式的簽名過程是「雜湊 + 加密」:
簽名時(產生 JWT):
- 對資料做「雜湊」→ 產生雜湊值(這步是單向的,不可逆)
- 用 Private Key「加密」這個雜湊值 → 這就是簽名(這步是可逆的)
因為只有 Private Key 能「加密」產生簽名,所以只有擁有 Private Key 的認證伺服器能產生有效的 JWT。
驗證時(驗證 JWT):
- 用 Public Key「解密」簽名 → 拿回原本的雜湊值
- 對資料重新做「雜湊」→ 產生新的雜湊值
- 比對兩個雜湊值是否一致
因為 Public Key 只能「解密」,不能「加密」,所以擁有 Public Key 的服務只能「驗證」JWT,無法偽造簽名。
這樣有什麼好處?
- Private Key 只有一個地方知道:只有負責產生 JWT 的認證伺服器需要知道
- Public Key 可以隨便給:即使被別人拿到也沒關係,因為它只能驗證,不能產生有效的 JWT
- 外洩風險降低:其他服務只有 Public Key,就算被入侵,攻擊者也無法偽造 JWT
常見演算法:RS256、RS384、RS512、ES256
適合的情境:有多個服務需要驗證 JWT、或是需要讓第三方驗證 JWT 的時候。
如何選擇?
| 情境 | 建議演算法 |
|---|---|
| 單一服務、內部系統 | HS256(對稱式) |
| 微服務架構 | RS256(非對稱式) |
| 需要第三方驗證 | RS256(非對稱式) |
| 效能優先 | HS256(對稱式) |
完整的 JWT 結構圖
flowchart TB
subgraph Header
H1[alg: HS256]
H2[typ: JWT]
end
subgraph Payload
P1[sub: 1234567890]
P2[name: John]
P3[exp: 1702900000]
end
subgraph Signature
S1[HMAC-SHA256]
end
Header -->|Base64Url| E1[eyJhbGci...]
Payload -->|Base64Url| E2[eyJzdWIi...]
E1 --> |\+| C[合併]
E2 --> |\+| C
C -->|\+ Secret| S1
S1 --> E3[SflKxwRJ...]
E1 --> J[JWT]
E2 --> J
E3 --> J
小結
現在你知道了:
- Header 描述 JWT 的元資料(演算法、類型)
- Payload 存放實際資料(Claims)
- Signature 用來驗證 JWT 沒有被竄改
- Base64 是編碼,不是加密,任何人都能解碼 Payload
- 絕對不要在 Payload 放機密資訊(密碼、信用卡號等)
- 對稱式演算法(HS256)用同一把密鑰簽名和驗證
- 非對稱式演算法(RS256)用私鑰簽名、公鑰驗證