本文 AI 產出,尚未審核
ExpressJS (TypeScript) – JWT 與 Auth 驗證
使用 jsonwebtoken 建立登入與驗證
簡介
在現代的 Web 應用程式中,身分驗證與授權是不可或缺的基礎建設。
傳統的 Session‑Cookie 方案雖然成熟,但在跨域、微服務或移動端應用中往往不夠彈性。
JSON Web Token(簡稱 JWT)則提供了 自包含(self‑contained)、無狀態(stateless) 的驗證機制,讓前端只需要攜帶一段簽名過的字串,即可完成身份認證與權限判斷。
本篇文章將以 ExpressJS + TypeScript 為基底,示範如何使用 jsonwebtoken 套件實作 登入(issue token)、保護路由(verify token),並探討常見的安全陷阱與最佳實踐。文章適合剛接觸 JWT 的初學者,也能為已有經驗的開發者提供實務參考。
核心概念
1. JWT 的結構與原理
JWT 由三個部份組成,使用 . 連接:
header.payload.signature
| 部分 | 內容 | 常見用途 |
|---|---|---|
| Header | 記錄演算法(alg)與類型(typ),例如 { "alg": "HS256", "typ": "JWT" } |
告訴接收端如何驗證簽名 |
| Payload | 放置聲明(claims),如使用者 ID、角色、過期時間等。 | 用於授權判斷 |
| Signature | 由 Header + Payload + 密鑰(secret)經過演算法產生的簽名 | 防止資料被竄改 |
重要:Payload 並非加密,任何人都可以 base64 解碼取得內容,不要在裡面放置敏感資訊(如密碼、信用卡號)。
2. 為什麼選擇 jsonwebtoken?
- ✅ 支援 HS256、RS256 等多種簽名演算法
- ✅ 完全 TypeScript 定義,開發時能得到型別提示
- ✅ 提供
sign()、verify()、decode()三大核心 API - ✅ 社群成熟、文件完整,適合作為教學與實務範例
3. 建立專案骨架
以下示範使用 npm、Express、TypeScript 與 dotenv(管理環境變數):
mkdir express-jwt-demo
cd express-jwt-demo
npm init -y
npm i express jsonwebtoken dotenv
npm i -D typescript @types/express @types/jsonwebtoken ts-node-dev
npx tsc --init
tsconfig.json 建議設定:
{
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist"
}
建立 .env(千萬別提交到 Git):
PORT=3000
JWT_SECRET=yourSuperSecretKey123!
JWT_EXPIRES_IN=1h # token 有效期限
4. 程式碼範例
4.1. 初始化 Express 與載入設定
// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use(express.json()); // 解析 JSON body
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
4.2. 建立 JWT 發行(Login)API
// src/routes/auth.ts
import { Router, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const router = Router();
// 模擬使用者資料庫(實務請改成 DB 查詢)
const fakeUser = {
id: 'u12345',
username: 'demo_user',
password: 'password123', // **僅示範用,實務請加密存放**
role: 'admin',
};
router.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
// 1️⃣ 驗證帳號密碼(簡化版)
if (username !== fakeUser.username || password !== fakeUser.password) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// 2️⃣ 建立 payload(只放必要資訊)
const payload = {
sub: fakeUser.id, // subject,通常放使用者唯一識別碼
name: fakeUser.username,
role: fakeUser.role,
};
// 3️⃣ 簽發 token
const token = jwt.sign(payload, process.env.JWT_SECRET as string, {
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
algorithm: 'HS256',
});
// 4️⃣ 回傳 token(建議放在 Authorization: Bearer <token>)
res.json({ accessToken: token });
});
export default router;
小技巧:
sub(subject)是 JWT 標準聲明之一,使用者 ID 放在此欄位,可在驗證時直接取用。
4.3. 建立驗證 Middleware
// src/middleware/authenticate.ts
import { Request, Response, NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
export interface AuthenticatedRequest extends Request {
user?: JwtPayload; // 讓後續路由能取得 token 內的資料
}
/**
* 驗證 JWT,若成功則把解碼後的 payload 加到 req.user
*/
export function authenticateToken(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) return res.sendStatus(401); // 無 token
jwt.verify(token, process.env.JWT_SECRET as string, (err, payload) => {
if (err) return res.sendStatus(403); // token 無效或過期
req.user = payload as JwtPayload;
next();
});
}
4.4. 保護路由 – 只允許已登入的使用者存取
// src/routes/protected.ts
import { Router } from 'express';
import { authenticateToken, AuthenticatedRequest } from '../middleware/authenticate';
const router = Router();
router.get('/profile', authenticateToken, (req: AuthenticatedRequest, res) => {
// 取得 JWT 中的使用者資訊
const user = req.user;
res.json({
message: '這是受保護的個人資訊',
user: {
id: user?.sub,
name: user?.name,
role: user?.role,
},
});
});
export default router;
4.5. 組合路由到主程式
// src/app.ts(續)
import authRouter from './routes/auth';
import protectedRouter from './routes/protected';
app.use('/api/auth', authRouter);
app.use('/api', protectedRouter);
完整流程:
- 前端呼叫
/api/auth/login並取得accessToken。- 之後每次請求受保護資源(如
/api/profile)時,於Authorization: Bearer <token>標頭帶上 token。authenticateToken中介軟體會驗證簽名、過期時間,並把解碼後的 payload 注入req.user,供後續路由使用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 密鑰外洩 | 若 JWT_SECRET 被寫死在程式碼或公開 repo,攻擊者可自行簽發合法 token。 |
使用環境變數或密鑰管理服務(如 AWS KMS、HashiCorp Vault),切勿硬編碼。 |
| 過長有效期限 | 長時間有效的 token 失竊後危害較大。 | 設定合理的 expiresIn(如 15 分鐘),搭配 Refresh Token 機制延長會話。 |
| 把敏感資料放入 Payload | Payload 可被解碼,非加密。 | 僅放 非敏感、必要 的資訊(id、role、email),如需加密可使用 JWE。 |
| 未驗證演算法 | 攻擊者可改寫 alg 為 none,繞過驗證(舊版庫曾有此漏洞)。 |
在 jwt.verify 時明確指定 algorithms: ['HS256'] 或使用 RSA/ECDSA。 |
| 忘記在前端清除 token | 登出時僅刪除前端儲存的 token,卻未讓伺服器失效,仍可能被盜用。 | 實作 黑名單(Blacklist)或 短期 Refresh Token,在登出時將 token 加入失效列表。 |
| 未處理錯誤回傳 | 直接回傳 500 讓前端無法分辨是授權失敗或系統錯誤。 | 統一錯誤格式,例如 { code: 401, message: 'Token expired' }。 |
最佳實踐小結:
- 使用強隨機密鑰(至少 256 位元)並定期輪換。
- 設定合理的過期時間,配合 Refresh Token。
- 在驗證時限定演算法,避免
none攻擊。 - 將 token 儲存在 HttpOnly、SameSite=Strict 的 Cookie(如果不想用 LocalStorage)。
- 在中介軟體中統一錯誤處理,讓前端易於呈現訊息。
實際應用場景
| 場景 | 使用方式 | 為何適合 JWT |
|---|---|---|
| 單頁應用(SPA) | 前端使用 fetch / axios 攜帶 Authorization: Bearer,後端驗證後返回資料。 |
無狀態、可跨域,減少伺服器儲存 Session 的開銷。 |
| 微服務系統 | 各服務間以 JWT 互相驗證呼叫者身份(如 API Gateway → Service A)。 | 不需要共享 Session Store,服務可獨立擴展。 |
| 行動 App | 手機端在登入後取得 JWT,之後每次 API 呼叫都帶上 token。 | 輕量、易於離線快取,且不依賴 Cookie。 |
| 第三方授權(OAuth2) | 授權伺服器返回 JWT 作為 Access Token。 | 標準化的 token 格式,支援多種授權模式。 |
總結
- JWT 以 自包含、無狀態 的特性,成為現代 Web / 行動應用的主流驗證方案。
- 使用
jsonwebtoken搭配 Express + TypeScript,只需幾行程式碼即可完成 登入、簽發 token、保護路由 的完整流程。 - 實務上要特別注意 密鑰管理、過期時間、演算法限制,以及 不要在 payload 中放敏感資訊。
- 透過 中介軟體 統一驗證與錯誤處理,能讓程式碼保持乾淨且易於維護。
掌握上述概念與範例後,你就能在自己的專案中安全、快速地導入 JWT 驗證,為使用者提供流暢且安全的登入體驗。祝開發順利! 🎉