本文 AI 產出,尚未審核

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 簽名,確保資料未被竄改。

驗證流程通常包括:

  1. Authorization Header 取得 Bearer <token>
  2. 使用相同的密鑰或公私鑰對 token 進行 解碼 + 驗證
  3. 檢查 exp(過期時間)與自訂的 claim(如 role)是否符合需求。
  4. 若驗證成功,將 payload 注入 req,讓路由可以直接使用。

2️⃣ 為什麼要在 Middleware 中加入 型別定義

TypeScript 的優勢在於 編譯期檢查,但 Express 原生的 Request 型別並沒有 user 屬性。若直接在程式內使用 req.user,編譯器會報錯。透過 宣告合併(declaration merging)自訂介面,我們可以為 Request 加上 user?: JwtPayload,讓 IDE 提供自動完成與型別安全。

3️⃣ 建立共用的 JWT 工具函式

簽發(sign)驗證(verify)解碼(decode) 的邏輯抽離成獨立模組,可避免在 Middleware 中寫太多重複程式碼,也方便單元測試。


程式碼範例

以下範例以 express@4typescript@5jsonwebtoken 為基礎,示範完整的 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 必須將 typeRootsinclude 包含此檔案,才能讓型別合併生效。

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(非對稱金鑰)並保管私鑰。

其他最佳實踐

  1. 統一錯誤回傳格式:例如 { code: 401, message: "...", error: "InvalidToken" },方便前端統一處理。
  2. 將授權(Authorization)與認證(Authentication)分離:Authentication 只負責驗證身份,Authorization 則在路由或服務層檢查 req.user.role 或其他權限。
  3. 使用 Refresh Token:若要支援長期登入,可在服務端保存 Refresh Token,並在 Access Token 過期時重新簽發。
  4. 單元測試:針對 authMiddlewareverifyToken 撰寫 Jest 測試,模擬不同錯誤情況(過期、簽名錯誤、缺少 Header)。
  5. 日誌與監控:對驗證失敗的請求寫入安全日誌,並透過監控平台檢測異常流量。

實際應用場景

場景 為何需要 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.ioconnection 事件中手動呼叫 verifyToken,將結果存於 socket.data.user
多租戶 SaaS 平台 同一平台服務多個客戶,每筆請求需辨識租戶 在 JWT payload 中加入 tenantIdauthMiddleware 解析後將 req.user.tenantId 注入,後續 DB 查詢依此過濾。

總結

  • Auth Middleware 是 Express + TypeScript 專案中保護資源的第一道防線。
  • 透過 JWT 的簽發與驗證,我們可以實作無狀態跨域、且可擴充的驗證機制。
  • 型別定義(declare merging)讓 req.user 在編譯期即得到安全保證,減少執行時錯誤。
  • 實務上要注意 密鑰管理、錯誤處理、角色授權,並配合 日誌、測試 形成完整的安全生態。

掌握以上概念與範例後,你就能在 Express + TypeScript 專案裡快速構建可靠的驗證層,為後續的功能開發奠定堅實的基礎。祝開發順利,安全第一! 🚀