ExpressJS (TypeScript) – JWT 與 Auth 驗證
單元:Auth Middleware 撰寫(含型別定義)
簡介
在 Node.js 生態系統裡,Express 是最常見的 Web 框架,而 JWT(JSON Web Token) 則是實作無狀態(stateless)驗證的首選方案。當我們在 TypeScript 專案中使用 JWT 時,最關鍵的環節就是 Auth Middleware——它負責從請求中抽取 Token、驗證其合法性、並把使用者資訊安全地注入到 req 物件供後續路由使用。
如果 Middleware 寫得不夠嚴謹,可能會導致 授權繞過、資訊洩漏 或 型別錯誤,在大型專案裡會成為難以追蹤的隱形漏洞。本文將一步步示範如何在 Express + TypeScript 中正確撰寫、型別化以及測試一個可靠的 Auth Middleware,讓讀者從「概念」走到「實務」的完整落地。
核心概念
1️⃣ JWT 的結構與驗證流程
- Header:描述演算法(如
HS256)與類型(JWT)。 - Payload:放置使用者的 claims(如
sub,exp,role)。 - Signature:使用密鑰對前兩段做 HMAC 或 RSA 簽名,確保資料未被竄改。
驗證流程通常包括:
- 從 Authorization Header 取得
Bearer <token>。 - 使用相同的密鑰或公私鑰對 token 進行 解碼 + 驗證。
- 檢查
exp(過期時間)與自訂的 claim(如role)是否符合需求。 - 若驗證成功,將 payload 注入
req,讓路由可以直接使用。
2️⃣ 為什麼要在 Middleware 中加入 型別定義?
TypeScript 的優勢在於 編譯期檢查,但 Express 原生的 Request 型別並沒有 user 屬性。若直接在程式內使用 req.user,編譯器會報錯。透過 宣告合併(declaration merging) 或 自訂介面,我們可以為 Request 加上 user?: JwtPayload,讓 IDE 提供自動完成與型別安全。
3️⃣ 建立共用的 JWT 工具函式
把 簽發(sign)、驗證(verify)、解碼(decode) 的邏輯抽離成獨立模組,可避免在 Middleware 中寫太多重複程式碼,也方便單元測試。
程式碼範例
以下範例以 express@4、typescript@5、jsonwebtoken 為基礎,示範完整的 Auth Middleware 開發流程。
3.1 初始化專案與安裝套件
npm init -y
npm i express jsonwebtoken dotenv
npm i -D typescript @types/express @types/jsonwebtoken ts-node-dev
npx tsc --init # 產生 tsconfig.json
記得在
.env中設定JWT_SECRET(或使用 RSA 金鑰)。
3.2 建立 JWT 工具 (src/utils/jwt.ts)
// src/utils/jwt.ts
import jwt, { JwtPayload, SignOptions, VerifyOptions } from "jsonwebtoken";
const secret = process.env.JWT_SECRET || "fallback-secret";
/**
* 產生 JWT
* @param payload - 任何想放入 token 的資料
* @param expiresIn - 有效期限,例如 '1h'、'7d'
*/
export function signToken(
payload: object,
expiresIn: string | number = "1h"
): string {
const options: SignOptions = { expiresIn };
return jwt.sign(payload, secret, options);
}
/**
* 驗證 JWT,回傳解碼後的 payload
* 若驗證失敗會拋出錯誤,呼叫端自行捕捉
*/
export function verifyToken(token: string): JwtPayload {
const options: VerifyOptions = { algorithms: ["HS256"] };
return jwt.verify(token, secret, options) as JwtPayload;
}
/**
* 只解碼,不驗證(僅在需要取得 header/payload 時使用)
*/
export function decodeToken(token: string): null | JwtPayload {
return jwt.decode(token) as null | JwtPayload;
}
3.3 為 Express Request 加上 user 型別(src/types/express.d.ts)
// src/types/express.d.ts
import { JwtPayload } from "jsonwebtoken";
declare global {
namespace Express {
interface Request {
/** 解析後的 JWT payload,若未驗證則為 undefined */
user?: JwtPayload;
}
}
}
注意:
tsconfig.json必須將typeRoots或include包含此檔案,才能讓型別合併生效。
3.4 Auth Middleware 本體 (src/middleware/auth.ts)
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import { verifyToken } from "../utils/jwt";
/**
* 解析 Authorization Header,驗證 JWT,並將 payload 注入 req.user
* 若驗證失敗,回傳 401 Unauthorized。
*/
export function authMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
// 1️⃣ 判斷 Header 是否存在
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({ message: "Missing or malformed token" });
return;
}
// 2️⃣ 取出 token 本體
const token = authHeader.split(" ")[1];
try {
// 3️⃣ 驗證 token,若失敗會拋錯
const payload = verifyToken(token);
// 4️⃣ 把 payload 寫入 req.user,型別已在 express.d.ts 中聲明
req.user = payload;
next(); // 繼續往下走
} catch (err) {
// 5️⃣ 捕捉錯誤;可根據錯誤類型返回更細緻的訊息
res.status(401).json({ message: "Invalid or expired token" });
}
}
3.5 在路由中使用 Middleware (src/routes/protected.ts)
// src/routes/protected.ts
import { Router, Request, Response } from "express";
import { authMiddleware } from "../middleware/auth";
const router = Router();
/**
* 只有通過 authMiddleware 的請求才能進入此路由
* 此時 req.user 已經被正確型別化,可直接存取
*/
router.get("/profile", authMiddleware, (req: Request, res: Response) => {
// 取得使用者資訊(假設 payload 中有 email、role)
const { email, role } = req.user ?? {};
res.json({
message: "成功取得使用者資料",
data: { email, role },
});
});
export default router;
3.6 完整的 Express 入口 (src/index.ts)
// src/index.ts
import express from "express";
import dotenv from "dotenv";
import protectedRouter from "./routes/protected";
dotenv.config();
const app = express();
app.use(express.json());
// 測試用的登入路由,簽發 token
app.post("/login", (req, res) => {
const { email } = req.body;
// 這裡僅示範,實務上應該先驗證帳號密碼
const token = signToken({ email, role: "user" }, "2h");
res.json({ token });
});
app.use("/api", protectedRouter);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
小結:以上程式碼展示了 從工具函式、型別定義、Middleware 到路由 的完整流程。只要把
authMiddleware加在需要保護的路由上,即可確保所有請求都已經過 JWT 驗證。
常見陷阱與最佳實踐
| 常見問題 | 為何會發生 | 解決方式 / Best Practice |
|---|---|---|
| Token 失效後仍能通過 | 忽略 exp 驗證或使用 jwt.decode(不驗證) |
一定使用 jwt.verify,並在錯誤處理時檢查 TokenExpiredError。 |
req.user 型別錯誤 |
未正確宣告合併或忘記 import JwtPayload |
在 express.d.ts 中 宣告全局,並確保 tsconfig.json 包含該檔案。 |
| 硬編碼密鑰 | 直接在程式碼內寫 secret = "my-secret" |
使用環境變數 (process.env.JWT_SECRET) 並在 .env 中管理。 |
| 未處理 Bearer 前綴 | 只取整個 Header,導致 "Bearer" 也被當作 token |
必須 檢查 authHeader.startsWith('Bearer ') 並 切割 取第二段。 |
| 跨域請求攜帶 Token 失敗 | 未在 CORS 設定 Access-Control-Allow-Credentials |
若前端使用 withCredentials: true,伺服器必須允許 Credentials 並正確設定 origin。 |
| Token 被竄改 | 使用弱演算法或共用公開金鑰 | 使用 HS256 + 強密鑰 或 RS256(非對稱金鑰)並保管私鑰。 |
其他最佳實踐
- 統一錯誤回傳格式:例如
{ code: 401, message: "...", error: "InvalidToken" },方便前端統一處理。 - 將授權(Authorization)與認證(Authentication)分離:Authentication 只負責驗證身份,Authorization 則在路由或服務層檢查
req.user.role或其他權限。 - 使用 Refresh Token:若要支援長期登入,可在服務端保存 Refresh Token,並在 Access Token 過期時重新簽發。
- 單元測試:針對
authMiddleware、verifyToken撰寫 Jest 測試,模擬不同錯誤情況(過期、簽名錯誤、缺少 Header)。 - 日誌與監控:對驗證失敗的請求寫入安全日誌,並透過監控平台檢測異常流量。
實際應用場景
| 場景 | 為何需要 Auth Middleware | 範例實作 |
|---|---|---|
| 會員系統的個人資料頁 | 必須確保只有登入者能存取自己的資料 | 在 GET /api/profile 前套用 authMiddleware,再根據 req.user.sub 讀取 DB。 |
| 管理後台 CRUD | 不同角色(admin、editor)擁有不同權限 | 在路由中先通過 authMiddleware,再寫 if (req.user.role !== 'admin') return res.status(403)。 |
| 微服務間的 JWT 轉發 | 前端只持有一次 JWT,後端服務間需要驗證身份 | 每個微服務都掛載相同的 authMiddleware,只要 token 有效即視為信任的內部呼叫。 |
| WebSocket 連線驗證 | WebSocket 握手時亦需驗證 JWT | 在 socket.io 的 connection 事件中手動呼叫 verifyToken,將結果存於 socket.data.user。 |
| 多租戶 SaaS 平台 | 同一平台服務多個客戶,每筆請求需辨識租戶 | 在 JWT payload 中加入 tenantId,authMiddleware 解析後將 req.user.tenantId 注入,後續 DB 查詢依此過濾。 |
總結
- Auth Middleware 是 Express + TypeScript 專案中保護資源的第一道防線。
- 透過 JWT 的簽發與驗證,我們可以實作無狀態、跨域、且可擴充的驗證機制。
- 型別定義(declare merging)讓
req.user在編譯期即得到安全保證,減少執行時錯誤。 - 實務上要注意 密鑰管理、錯誤處理、角色授權,並配合 日誌、測試 形成完整的安全生態。
掌握以上概念與範例後,你就能在 Express + TypeScript 專案裡快速構建可靠的驗證層,為後續的功能開發奠定堅實的基礎。祝開發順利,安全第一! 🚀