【後端架構系列 02】解剖 JWT:從原理到 Node.js 實作的避坑指南
01. 前言:不要當 Copy-Paste 工程師
在上一篇 【後端架構系列 01】驗證演化論 中,我們釐清了 Session 與 JWT 的架構差異。今天,我們要捲起袖子寫 Code 了。
很多新手的起手式是去 StackOverflow 複製一段 jwt.sign() 的代碼,能跑就不管了。但你知道嗎?JWT 其實是一個裸奔的資料格式。 如果你不明就裡地把使用者的 Email、甚至是權限設定直接塞進去,你其實正在網路上廣播你的資料庫結構。
這篇文章,我們要深入 JWT 的肌肉與骨骼(Structure),理解它的靈魂(Signature),最後用 Node.js + TypeScript 打造一個安全、可維護的驗證模組。
02. JWT 的解剖學:它不是加密,是編碼
JWT (JSON Web Token) 的外觀通常是這樣的:aaaaa.bbbbb.ccccc。
這三個部分由 . 分隔,分別是:
- Header (標頭):我是誰?用什麼演算法?
- Payload (內容):我要傳遞什麼資料?
- Signature (簽章):證明我沒被改過。
致命誤區:Base64Url 不是加密!
請看下面這個 Payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
經過 Base64Url 編碼後,它會變成一串看似亂碼的字串。但請注意,這過程完全可逆,不需要任何金鑰。 任何人拿到這串字串,丟進 Base64 解碼器,都能看到 admin: true。
👨💻 筆者警告: 絕對不要 在 Payload 中放入敏感資訊(如密碼、身分證號、信用卡號)。 JWT 的設計目的是「防篡改 (Integrity)」,而不是「防竊聽 (Confidentiality)」。如果你需要傳輸敏感資料,請使用 JWE (JSON Web Encryption) 或走 HTTPS 加密通道。
03. 簽章的數學魔法:HMAC 的力量
JWT 為什麼安全?關鍵在於第三段的 Signature。
它的產生公式如下:
$$Signature = HMACSHA256( base64Url(Header) + "." + base64Url(Payload), your_secret_key )$$
運作原理
- Server 手上有一把
secret_key(例如:"my-super-secret")。 - 當 Server 發 Token 給 User 時,它把 Header 和 Payload 接起來,用
secret_key攪拌一下(雜湊),算出簽章。 - User 下次帶 Token 來時,Server 把 Header 和 Payload 再拿出來,用同樣的
secret_key再算一次。 - 比對: 如果算出來的結果跟 Token 第三段一樣,代表資料沒被改過。
如果駭客把 Payload 裡的 admin: false 改成 true,但他不知道 secret_key,他就無法算出對應的新簽章。Server 一比對就會發現:「嘿!你的簽章跟內容對不上,滾!」
04. 實戰工坊:Node.js + TypeScript 實作
我們不只要會用 jsonwebtoken,還要封裝成符合企業級開發規範的工具。
環境準備
npm install express jsonwebtoken dotenv
npm install -D typescript @types/express @types/jsonwebtoken @types/node
步驟一:定義型別與設定檔 (Strong Typing)
不要在代碼裡到處寫 process.env.JWT_SECRET,這很難維護。
// src/config/index.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
jwt: {
secret: process.env.JWT_SECRET || 'default_secret_please_change_me',
accessExpiration: '15m', // Access Token 短效期
refreshExpiration: '7d', // Refresh Token 長效期
},
};
步驟二:封裝 JWT Utils (核心邏輯)
我們會建立一個 jwt.utils.ts,這裡應用了 單一職責原則 (SRP),把簽發與驗證邏輯集中管理。
// src/utils/jwt.utils.ts
import jwt, { SignOptions } from 'jsonwebtoken';
import { config } from '../config';
// 定義我們 Token 裡具體要存什麼,這能讓 TypeScript 幫我們檢查
export interface TokenPayload {
userId: string;
role: string;
email: string;
}
/**
* 簽發 Access Token
* @param payload - 使用者資訊
*/
export const signAccessToken = (payload: TokenPayload): string => {
const signInOptions: SignOptions = {
expiresIn: config.jwt.accessExpiration,
algorithm: 'HS256' // 明確指定演算法,防止 None Algorithm 攻擊
};
return jwt.sign(payload, config.jwt.secret, signInOptions);
};
/**
* 驗證 Token
* @param token - 從 Header 拿到的字串
*/
export const verifyToken = (token: string): TokenPayload | null => {
try {
const decoded = jwt.verify(token, config.jwt.secret) as TokenPayload;
return decoded;
} catch (error) {
// 這裡可以細分錯誤類型:TokenExpiredError 或 JsonWebTokenError
return null;
}
};
👨💻 筆者點評: 注意我在
jwt.sign時明確指定了algorithm: 'HS256'。 歷史上有一個著名的漏洞叫 "None Algorithm Attack"。早期有些 Library 允許 Header 設定{"alg": "none"},Server 看到這個設定就會跳過簽章驗證。駭客只要把 Token Header 改成 None,就可以偽造任意身分。雖然現代 Library 多已修復,但明確指定演算法是個好習慣。
步驟三:製作 Auth Middleware (守門員)
接下來我們要把它掛載到 Express 的路由上。
// src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken, TokenPayload } from '../utils/jwt.utils';
// 擴充 Express Request 型別,讓後面的 Controller 可以直接用 req.user
declare global {
namespace Express {
interface Request {
user?: TokenPayload;
}
}
}
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
// 1. 從 Header 取出 Token
// 格式通常是: Authorization: Bearer <token>
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供 Token 或格式錯誤' });
}
const token = authHeader.split(' ')[1];
// 2. 驗證 Token
const payload = verifyToken(token);
if (!payload) {
return res.status(403).json({ message: 'Token 無效或已過期' });
}
// 3. 驗證成功,將資料掛載到 req 物件
req.user = payload;
// 4. 放行
next();
};
05. 安全性補完計畫
有了上面的代碼,你的 JWT 系統已經比 80% 的網上教學更安全了。但為了達到 Production Ready,我們還需要注意以下幾點:
1. Secret Key 的強度
你的 JWT_SECRET 就像你的銀行密碼。不要用 secret123 這種爛密碼。請使用 openssl rand -base64 32 生成一個高強度的隨機字串。如果 Key 被暴力破解,你的整個驗證系統就崩塌了。
2. Token 的儲存位置
這是一個千古難題:LocalStorage vs Cookie。
- LocalStorage: 容易被 XSS (跨站腳本攻擊) 竊取。如果你的網站有漏洞讓駭客能執行 JS,他們就能
localStorage.getItem('token')把你的 Token 偷走。 - Cookie (HttpOnly): JS 讀不到,防 XSS,但容易被 CSRF (跨站請求偽造) 攻擊。
筆者推薦方案: 對於一般專案,Access Token 存記憶體 (JS 變數),Refresh Token 存 HttpOnly Cookie 是目前公認最安全的折衷方案。這部分我們將在下一篇詳細實作。
06. 下集預告:Token 的輪迴與重生
現在我們能簽發 Token 了,但如果 Token 過期了怎麼辦?讓使用者重登嗎?體驗太差了。
如果我們把 Token 有效期設長一點(例如 30 天),那如果使用者的手機丟了,我們怎麼讓他「強制登出」?(記得嗎,JWT 是 Stateless 的)。
這就是 Refresh Token 與 Redis 登場的時候了。
在下一篇 【Ep.3:雙 Token 機制與 Redis 黑名單實戰】 中,我們將構建一個完整的 Token 生命週期管理系統,實現「無感換證」與「一鍵踢人」的高級功能。
程式碼可以複製,但觀念必須內化。試著自己把
verifyToken的邏輯寫一遍,你會發現更多細節。

關於作者
Ken Huang
擁有超過 9 年軟體開發經驗的資深工程師,現任 APP 工程師,專精於 Android / iOS 雙平台開發。同時持續拓展後端與雲端技術範疇,致力於朝向全端架構師的領域邁進。
具備完整的專案生命週期實戰經驗:
- 新創開發:主導多個從零到一的新專案規劃、技術選型與開發落地。
- 系統維護與重構:擁有維護與升級 10 年以上大型歷史專案(Legacy Code)的豐富經驗,擅長進行代碼重構與效能優化。
身為知名 PTT 第三方 APP BePTT 的開發者,致力於將 Clean Architecture、Scrum 敏捷開發與現代化軟體工程原則應用於產品中,打造極致的使用者體驗。
verb.tw 是我紀錄技術軌跡與架構思考的基地,內容涵蓋 App 開發實戰、全端技術整合與軟體工程最佳實踐。
系列文章目錄
- 【後端架構系列 01】驗證演化論:從 Session 到 JWT 的技術博弈
- 【後端架構系列 02】解剖 JWT:從原理到 Node.js 實作的避坑指南 (本文)
- 【後端架構系列 03】雙 Token 機制與 Redis:打造可撤銷的完美驗證
- 【後端架構系列 04】前端生存指南:Axios 攔截器與無感換證 (Silent Refresh)