本文 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. 建立專案骨架

以下示範使用 npmExpressTypeScriptdotenv(管理環境變數):

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);

完整流程

  1. 前端呼叫 /api/auth/login 並取得 accessToken
  2. 之後每次請求受保護資源(如 /api/profile)時,於 Authorization: Bearer <token> 標頭帶上 token。
  3. authenticateToken 中介軟體會驗證簽名、過期時間,並把解碼後的 payload 注入 req.user,供後續路由使用。

常見陷阱與最佳實踐

陷阱 說明 解決方案
密鑰外洩 JWT_SECRET 被寫死在程式碼或公開 repo,攻擊者可自行簽發合法 token。 使用環境變數或密鑰管理服務(如 AWS KMS、HashiCorp Vault),切勿硬編碼。
過長有效期限 長時間有效的 token 失竊後危害較大。 設定合理的 expiresIn(如 15 分鐘),搭配 Refresh Token 機制延長會話。
把敏感資料放入 Payload Payload 可被解碼,非加密。 僅放 非敏感必要 的資訊(id、role、email),如需加密可使用 JWE。
未驗證演算法 攻擊者可改寫 algnone,繞過驗證(舊版庫曾有此漏洞)。 jwt.verify 時明確指定 algorithms: ['HS256'] 或使用 RSA/ECDSA。
忘記在前端清除 token 登出時僅刪除前端儲存的 token,卻未讓伺服器失效,仍可能被盜用。 實作 黑名單(Blacklist)或 短期 Refresh Token,在登出時將 token 加入失效列表。
未處理錯誤回傳 直接回傳 500 讓前端無法分辨是授權失敗或系統錯誤。 統一錯誤格式,例如 { code: 401, message: 'Token expired' }

最佳實踐小結

  1. 使用強隨機密鑰(至少 256 位元)並定期輪換。
  2. 設定合理的過期時間,配合 Refresh Token。
  3. 在驗證時限定演算法,避免 none 攻擊。
  4. 將 token 儲存在 HttpOnly、SameSite=Strict 的 Cookie(如果不想用 LocalStorage)。
  5. 在中介軟體中統一錯誤處理,讓前端易於呈現訊息。

實際應用場景

場景 使用方式 為何適合 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 驗證,為使用者提供流暢且安全的登入體驗。祝開發順利! 🎉