seansie's blog

三分鐘掌握 Express.js 權限驗證:JWT 與 RBAC 中介軟體實作範例

在 Express.js(ES Modules 模式)中處理基於 RBAC(Role-Based Access Control,基於角色的存取控制) 的使用者登入與權限驗證架構,通常分為四個核心層次:

  1. 認證(Authentication):驗證身分(如帳號/密碼)並簽發權杖(如 JWT 或 Session Cookie)。
  2. 認證中介軟體(Auth Middleware):解析並驗證 Token,將解密後的 user 物件附加至 req.user
  3. 授權中介軟體(RBAC Middleware):檢查 req.user.role 或使用者權限清單是否具備存取該路由的資格。
  4. 路由保護(Route Guarding):將上述中介軟體串接至受保護的 API 端點。

1. 專案配置 (package.json)

確保 package.json 中啟用了 "type": "module",以原生支援 ES Modules (import/export)。

{
  "name": "express-rbac-esm",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.19.2",
    "jsonwebtoken": "^9.0.2"
  }
}

2. 核心程式碼實作 (server.js)

import express from 'express';
import jwt from 'jsonwebtoken';

const app = express();
app.use(express.json());

const JWT_SECRET = 'your_jwt_secret_key_placeholder';

// ==========================================
// 1. RBAC 權限角色定義與驗證 Placeholder 函數
// ==========================================

export const ROLES = Object.freeze({
  ADMIN: 'ADMIN',
  MANAGER: 'MANAGER',
  USER: 'USER',
});

/**
 * 驗證使用者帳密 Placeholder
 * 實際環境中應替換為資料庫查詢與密碼雜湊比對(如 bcrypt.compare)
 * 
 * @param {string} username 
 * @param {string} password 
 * @returns {Promise<{ id: string, username: string, role: string } | null>}
 */
async function verifyCredentialsPlaceholder(username, password) {
  // 模擬資料庫查詢延遲
  await new Promise((resolve) => setTimeout(resolve, 50));

  // 測試用假資料
  const mockUsers = [
    { id: 'u_1', username: 'admin_user', password: 'password123', role: ROLES.ADMIN },
    { id: 'u_2', username: 'manager_user', password: 'password123', role: ROLES.MANAGER },
    { id: 'u_3', username: 'normal_user', password: 'password123', role: ROLES.USER },
  ];

  const foundUser = mockUsers.find((u) => u.username === username && u.password === password);
  if (!foundUser) return null;

  return { id: foundUser.id, username: foundUser.username, role: foundUser.role };
}

// ==========================================
// 2. 中介軟體 (Middlewares)
// ==========================================

/**
 * 身分驗證中介軟體:解析 Bearer Token 並掛載至 req.user
 */
export function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // 格式: Bearer <TOKEN>

  if (!token) {
    return res.status(401).json({ error: 'Unauthorized: 未提供 Access Token' });
  }

  jwt.verify(token, JWT_SECRET, (err, decodedUser) => {
    if (err) {
      return res.status(403).json({ error: 'Forbidden: Token 無效或已過期' });
    }
    req.user = decodedUser;
    next();
  });
}

/**
 * RBAC 授權中介軟體生成器:檢查使用者是否具備指定角色
 * @param {string[]} allowedRoles - 允許存取的角色陣列
 */
export function authorizeRoles(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user || !req.user.role) {
      return res.status(403).json({ error: 'Forbidden: 無法確認使用者身分與權限' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ 
        error: 'Forbidden: 權限不足',
        requiredRoles: allowedRoles,
        currentRole: req.user.role 
      });
    }

    next();
  };
}

// ==========================================
// 3. API 路由定義
// ==========================================

// 登入路由
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;

  if (!username || !password) {
    return res.status(400).json({ error: '請提供帳號與密碼' });
  }

  try {
    // 呼叫驗證 Placeholder
    const user = await verifyCredentialsPlaceholder(username, password);

    if (!user) {
      return res.status(401).json({ error: '帳號或密碼錯誤' });
    }

    // 簽發包含 Role 資訊的 JWT
    const payload = { id: user.id, username: user.username, role: user.role };
    const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '2h' });

    return res.json({
      message: '登入成功',
      accessToken: token,
      user: payload
    });
  } catch (error) {
    return res.status(500).json({ error: '伺服器內部錯誤' });
  }
});

// 公開/一般受保護端點(所有已登入角色皆可存取)
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ message: '個人檔案資料', user: req.user });
});

// 管理端點(僅限 MANAGER 或 ADMIN 存取)
app.get('/api/reports', authenticateToken, authorizeRoles(ROLES.MANAGER, ROLES.ADMIN), (req, res) => {
  res.json({ message: '管理報表數據(Manager 與 Admin 可見)' });
});

// 最高權限端點(僅限 ADMIN 存取)
app.delete('/api/users/:id', authenticateToken, authorizeRoles(ROLES.ADMIN), (req, res) => {
  res.json({ message: `使用者 ${req.params.id} 已由系統管理員刪除` });
});

// 啟動伺服器
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

3. RBAC 架構實作重點解析

組件 職責 說明
verifyCredentialsPlaceholder 身分比對 封裝資料庫查詢與密碼驗證邏輯,回傳包含 role 欄位的使用者實體。
authenticateToken 身分鑑別 (AuthN) 攔截請求並解析 HTTP Header 中的 JWT,將 Payload 儲存至 req.user
authorizeRoles(...roles) 權限判定 (AuthZ) 高階函式(Higher-Order Function),依據各路由定義傳入白名單角色,驗證 req.user.role
Token Payload 狀態傳遞 role 內嵌於 JWT Payload 中,使中介軟體無需每次存取資料庫即可完成粗粒度(Coarse-grained)角色驗證。