三分鐘掌握 Express.js 權限驗證:JWT 與 RBAC 中介軟體實作範例
在 Express.js(ES Modules 模式)中處理基於 RBAC(Role-Based Access Control,基於角色的存取控制) 的使用者登入與權限驗證架構,通常分為四個核心層次:
- 認證(Authentication):驗證身分(如帳號/密碼)並簽發權杖(如 JWT 或 Session Cookie)。
- 認證中介軟體(Auth Middleware):解析並驗證 Token,將解密後的 user 物件附加至
req.user。 - 授權中介軟體(RBAC Middleware):檢查
req.user.role或使用者權限清單是否具備存取該路由的資格。 - 路由保護(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)角色驗證。 |