我在 JavaScript 解碼 JWT 時,第一個原則不是「怎麼把 payload 印出來」,而是先把三個狀態分清楚:decoded、cryptographically verified、accepted by my application。只要 header 與 payload 能被解析,最多只能證明我讀到了 token 自己聲稱的內容,不能證明簽章正確,也不能證明 API 應該接受它。
如果只是快速檢查,我會直接使用 JWT Decoder。需要在程式裡重現相同行為時,我會按照固定順序處理。
我解碼 JWT 的順序
- 移除可能存在的
Bearer或Authorization: Bearer前綴。 - 先確認 compact token 的 segment 數量。
- 把 Base64URL 轉回 bytes。
- 以 UTF-8 解碼 bytes。
- 明確處理 JSON parsing error。
- 檢查
alg、kid、iss、aud、exp,但不把它們當成已驗證資料。
這個順序可以把「輸入格式錯誤」「Base64URL 錯誤」「UTF-8 錯誤」「JSON 錯誤」分開,而不是全部變成一個模糊的 parse failure。
先把輸入正規化
實際除錯時我常拿到的是:
Bearer eyJ...
甚至:
Authorization: Bearer eyJ...
我會先整理成純 compact token:
function normalizeJwtInput(input) {
return input
.trim()
.replace(/^authorization\s*:\s*bearer\s+/i, "")
.replace(/^bearer\s+/i, "")
.trim()
.replace(/^['"]|['"]$/g, "");
}
這一步只做格式清理,不推測 token 是否可信。
先檢查 segment 數量
一般的 signed JWT/JWS compact form 是:
header.payload.signature
通常有三段。compact JWE 則通常有五段。我不會直接寫:
JSON.parse(atob(token.split(".")[1]))
而是先確認結構:
function splitJwt(input) {
const token = normalizeJwtInput(input);
const parts = token.split(".");
if (parts.length === 5) {
throw new Error("看起來是 encrypted JWE,不是 3-part signed JWT");
}
if (parts.length !== 3) {
throw new Error(`預期 3 個 JWT segments,實際為 ${parts.length}`);
}
return parts;
}
Base64URL 不是普通 Base64
JWT segment 使用 URL-safe alphabet,- 與 _ 取代 + 與 /,padding = 也常被省略。因此我會先轉換 alphabet 並補 padding。
function base64UrlToBytes(segment) {
const base64 = segment
.replace(/-/g, "+")
.replace(/_/g, "/");
const padded = base64.padEnd(
base64.length + ((4 - (base64.length % 4)) % 4),
"="
);
const binary = atob(padded);
return Uint8Array.from(binary, char => char.charCodeAt(0));
}
atob() 只把編碼內容轉成 byte-like binary string,它本身沒有解決 UTF-8。
我明確用 UTF-8 解 JSON
JWT JSON 可以包含中文、日文、人名或其他非 ASCII 內容。如果只在純 ASCII 測試資料上驗證 one-liner,很容易誤以為程式永遠沒問題。
function decodeJsonSegment(segment, label) {
let text;
try {
const bytes = base64UrlToBytes(segment);
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch {
throw new Error(`${label} 不是有效的 Base64URL/UTF-8 資料`);
}
let value;
try {
value = JSON.parse(text);
} catch {
throw new Error(`${label} 不是有效 JSON`);
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} 必須解碼成 JSON object`);
}
return value;
}
組成最小 decoder
function decodeJwt(input) {
const [headerSegment, payloadSegment, signature] = splitJwt(input);
return {
header: decodeJsonSegment(headerSegment, "JWT header"),
payload: decodeJsonSegment(payloadSegment, "JWT payload"),
signature,
};
}
這時我可以讀:
const decoded = decodeJwt(token);
console.log(decoded.header.alg);
console.log(decoded.header.kid);
console.log(decoded.payload.iss);
console.log(decoded.payload.aud);
console.log(decoded.payload.exp);
但我會在 UI 或 log 明確標示 decoded only。
exp 最大的坑是秒與毫秒
JWT NumericDate 使用 Unix epoch 秒。JavaScript Date.now() 是毫秒。
所以:
const expired = Date.now() >= payload.exp * 1000;
const expiresAt = new Date(payload.exp * 1000);
我不會寫:
Date.now() >= payload.exp
如果主要問題是 token 時間,我會用 JWT Expiry Checker 同時檢查 iat、nbf、exp 與有效區間。
在 Node.js 我通常使用 Buffer
function decodeBase64UrlJson(segment) {
return JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
}
function decodeJwtNode(token) {
const parts = token.trim().split(".");
if (parts.length !== 3) throw new Error("需要 3-part signed JWT");
return {
header: decodeBase64UrlJson(parts[0]),
payload: decodeBase64UrlJson(parts[1]),
signature: parts[2],
};
}
這仍然只是 inspection。伺服器真正要做 authentication 時,我會使用成熟的 JOSE/JWT library,並指定 allowed algorithm、issuer、audience 與 time policy。
使用 jwt-decode 時我仍保留同樣邊界
import { jwtDecode } from "jwt-decode";
const payload = jwtDecode(token);
const header = jwtDecode(token, { header: true });
jwt-decode 的用途就是 decode,不會因為 package 成功回傳 object,就自動變成 signature verification。
為什麼我避免 one-liner
JSON.parse(atob(token.split(".")[1]));
我不把它當可重用實作,因為它:
- 沒確認 segment 是否存在;
- 沒明確處理 Base64URL;
- 沒處理 padding;
- 可能錯誤處理 UTF-8;
- 錯誤訊息很差;
- 忽略 header 的
alg、kid; - 最容易讓人忘記 signature 根本還沒驗證。
Decode 後我下一步做什麼
如果我要知道 token 是否由可信任 key 簽發,我會使用 JWT Signature Verifier 或 JWKS Validator。
我把流程分成:
decode
→ 讀取 header/payload
verify signature
→ trusted key 是否真的簽了它
validate claims
→ iss/aud/exp/nbf 是否符合政策
authorize
→ scope/role 是否允許目前操作
JWT Validator 與 JWT Debugger 可以補上結構、時間與 401 排查。
我在瀏覽器處理 JWT 的原則
即使 payload 可讀,access token 仍然是 credential。我會:
- 不把 JWT 放進 URL;
- 不把完整 production bearer token 寫進 console/log;
- 不為了除錯把 token 存進 localStorage/sessionStorage;
- 優先使用 synthetic 或 redacted token;
- 不為了單純 Base64URL decode 把 token 傳到伺服器。
JWTDecoder.tools 的 decoder 也是以瀏覽器記憶體內處理為原則。
我的 JavaScript JWT 解碼清單
- 清理 Bearer 前綴。
- 檢查 compact segment 數量。
- 正確轉換 Base64URL。
- 用 UTF-8 解 bytes。
- 分別處理 JSON error。
- 查看
alg、kid、iss、aud與時間 claims。 - JavaScript 處理 NumericDate 時乘以 1000。
- 把結果稱為 decoded,而不是 verified。
- 需要 trust 時使用已知 key/JWKS 做 signature verification。
- 需要 acceptance 時再檢查 issuer、audience、expiration、token type 與 permissions。
對我來說,JavaScript 解碼 JWT 最重要的不是把程式壓縮成一行,而是讓 readable、verified、acceptable 三個狀態永遠不會被混在一起。