今天我們將使用 Node.js + Express + MongoDB 打造一個具備 RBAC 權限架構的 Blog 系統 API。
本篇教學會一步步實作從「登入、發文、權限驗證」的API開發,
並完成 RBAC(Role-Based Access Control)角色基礎權限控管 的後端實作。
要讓我們的系統能夠根據不同角色(Role),限制使用者能執行的操作範圍。
以下是這個 Blog API 的功能與權限設定:
功能 | 權限 |
---|---|
註冊 / 登入 | 所有人 |
查看文章(GET /posts) | 所有登入者 |
新增文章(POST /posts) | editor , admin |
編輯文章(PUT /posts/:id) | editor , admin |
刪除文章(DELETE /posts/:id) | 僅限 admin |
這樣的架構能讓權限管理更清楚,開發人員只需在中介層設定角色限制,
就能避免程式中出現大量「if 判斷式」的邏輯。
npm init -y
npm install express mongoose bcrypt jsonwebtoken cors
我們使用兩個模型:
User
— 用於帳號與角色管理Post
— 用於文章 CRUD 操作在簡化版本中,我們直接將角色存在 User 模型中;
若是大型系統,建議分離成
Role
與Permission
表,以便動態維護。
// models/user.model.js
import mongoose from "mongoose";
import bcrypt from "bcrypt";
const userSchema = new mongoose.Schema({
username: { type: String, unique: true },
password: String,
role: { type: String, enum: ["user", "editor", "admin"], default: "user" },
});
// 自動加密密碼
userSchema.pre("save", async function (next) {
if (this.isModified("password"))
this.password = await bcrypt.hash(this.password, 10);
next();
});
export const User = mongoose.model("User", userSchema);
// models/post.model.js
import mongoose from "mongoose";
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
createdAt: { type: Date, default: Date.now },
});
export const Post = mongoose.model("Post", postSchema);
使用者登入成功後,我們會發出一個 JWT Token 作為憑證。
這個 Token 會夾帶使用者的 id
與 role
,後續請求時用於身分驗證。
// routes/auth.js
import express from "express";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { User } from "../models/user.model.js";
const router = express.Router();
const SECRET = "MY_JWT_SECRET";
// 註冊
router.post("/register", async (req, res) => {
const { username, password, role } = req.body;
const exists = await User.findOne({ username });
if (exists) return res.status(400).json({ error: "使用者已存在" });
const user = await User.create({ username, password, role });
res.json({
message: "註冊成功",
user: { username: user.username, role: user.role },
});
});
// 登入
router.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) return res.status(404).json({ error: "找不到帳號" });
const ok = await bcrypt.compare(password, user.password);
if (!ok) return res.status(401).json({ error: "密碼錯誤" });
const token = jwt.sign(
{ id: user._id, role: user.role },
SECRET,
{ expiresIn: "1h" }
);
res.json({ message: "登入成功", token });
});
export default router;
這是 RBAC 的核心邏輯。
在這裡,我們將請求拆為兩層檢查:
1️⃣ verifyToken — 驗證使用者是否能登入。
2️⃣ authorizeRoles — 驗證使用者角色是否符合權限。
// middleware/auth.js
import jwt from "jsonwebtoken";
const SECRET = "MY_JWT_SECRET";
// 驗證登入狀態
export function verifyToken(req, res, next) {
const auth = req.headers.authorization;
if (!auth) return res.status(401).json({ error: "未授權,請登入" });
const token = auth.split(" ")[1];
jwt.verify(token, SECRET, (err, decoded) => {
if (err) return res.status(403).json({ error: "Token 無效或過期" });
req.user = decoded;
next();
});
}
// 驗證角色
export function authorizeRoles(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role))
return res.status(403).json({ error: "權限不足,無法執行此操作" });
next();
};
}
authorizeRoles() 可以接收多個角色。
例如 authorizeRoles("editor", "admin")
允許這兩個角色使用該 API。
接著建立文章 API,並根據角色設定不同的操作權限。
// routes/posts.js
import express from "express";
import { Post } from "../models/post.model.js";
import { verifyToken, authorizeRoles } from "../middleware/auth.js";
const router = express.Router();
// 查詢所有文章(登入者皆可)
router.get("/", verifyToken, async (req, res) => {
try {
const posts = await Post.find().populate("author", "username role");
res.json(posts);
} catch (err) {
res.status(500).json({ error: "伺服器錯誤,無法取得文章" });
}
});
// 新增文章(editor, admin)
router.post("/", verifyToken, authorizeRoles("editor", "admin"), async (req, res) => {
try {
const { title, content } = req.body;
if (!title) return res.status(400).json({ error: "標題不可為空" });
const post = await Post.create({
title,
content,
author: req.user.id,
});
res.status(201).json({ message: "文章建立成功", post });
} catch (err) {
res.status(500).json({ error: "伺服器錯誤,建立文章失敗" });
}
});
// 編輯文章(editor, admin)
router.put("/:id", verifyToken, authorizeRoles("editor", "admin"), async (req, res) => {
try {
// 先查詢文章,確認存在並檢查權限
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: "找不到要更新的文章" });
}
// 資料層權限檢查:
// 1. admin 可以編輯任何文章
// 2. editor 只能編輯自己的文章
if (req.user.role !== "admin" && post.author.toString() !== req.user.id) {
return res.status(403).json({ error: "無權限編輯此文章" });
}
// 權限驗證通過後,才執行更新
const updatedPost = await Post.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
res.json({ message: "文章已更新", post: updatedPost });
} catch (err) {
res.status(500).json({ error: "伺服器錯誤,更新文章失敗" });
}
});
// 刪除文章(admin only)
router.delete("/:id", verifyToken, authorizeRoles("admin"), async (req, res) => {
try {
const post = await Post.findByIdAndDelete(req.params.id);
if (!post) return res.status(404).json({ error: "找不到要刪除的文章" });
res.json({ message: "文章已刪除", deletedId: post._id });
} catch (err) {
res.status(500).json({ error: "伺服器錯誤,刪除失敗" });
}
});
export default router;
// server.js
import express from "express";
import mongoose from "mongoose";
import cors from "cors";
import authRoutes from "./routes/auth.js";
import postRoutes from "./routes/posts.js";
const app = express();
app.use(express.json());
app.use(cors());
await mongoose.connect("mongodb://localhost:27017/blog_rbac_demo");
console.log("✅ MongoDB 已連線");
app.use("/auth", authRoutes);
app.use("/posts", postRoutes);
app.listen(3000, () => console.log("🚀 Server 已啟動:http://localhost:3000"));
[登入 → 簽發 JWT Token]
↓
[每次請求帶 Authorization: Bearer <token>]
↓
[verifyToken() 驗證登入狀態]
↓
[authorizeRoles() 比對角色權限]
↓
允許 ✅ 或 拒絕 ❌
↓
執行 API(新增 / 編輯 / 刪除)
註冊帳號到DB裡
角色 | username | password |
---|---|---|
user | alice | 123456 |
editor | bob | 123456 |
admin | root | 123456 |
POST /auth/register
{
"username": "alice",
"password": "123456"
"role":"user"
}
{
"username": "bob",
"password": "123456"
"role":"editor"
}
{
"username": "root",
"password": "123456",
"role":"admin"
}
登入取得 Token
POST /auth/login
{
"username": "bob",
"password": "123456"
}
呼叫 API
API | 角色可執行 | 結果 |
---|---|---|
GET /posts | user, editor, admin | ✅ |
POST /posts | editor, admin | ✅ |
PUT /posts/:id | editor, admin | ✅ |
DELETE /posts/:id | admin only | ✅(其餘 403) |
這次的 Blog RBAC 實作,示範了如何在後端完成「身分驗證 + 授權控制」的完整流程。
透過 JWT 驗證使用者身份,再結合角色權限判斷,成功建立了一套權限架構。
今天的開發內容,已經涵蓋了 RBAC 三個實務層面中的前兩個:
1️⃣ 功能層(Feature-Level)
→ 控制使用者能否操作特定 API,例如「新增文章」或「刪除用戶」。
2️⃣ 資料層(Data-Level)
→ 控制不同角色可操作的資料範圍,例如:「編輯只能修改自己發表的文章」、「管理員可編輯或刪除全部內容」。
接下來要挑戰的是第三個層面:
3️⃣ 畫面層(UI-Level)
→ 在前端根據角色動態調整顯示內容,例如 admin 才能看到「管理權限」按鈕。
明天,我們將繼續進行 前端 RBAC 實作篇,
讓畫面也能根據使用者角色自動變化,實現真正完整的「全端權限系統」。