iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Modern Web

現在就學Node.js系列 第 26

RBAC 角色權限控管(上)— Blog API 實作篇 -Day26

  • 分享至 

  • xImage
  •  

今天我們將使用 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 判斷式」的邏輯。

Step 1:初始化專案環境

npm init -y
npm install express mongoose bcrypt jsonwebtoken cors

Step 2:建立資料模型(Models)

我們使用兩個模型:

  • User — 用於帳號與角色管理
  • Post — 用於文章 CRUD 操作

在簡化版本中,我們直接將角色存在 User 模型中;

若是大型系統,建議分離成 RolePermission 表,以便動態維護。

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

Step 3:建立註冊與登入 API(JWT)

使用者登入成功後,我們會發出一個 JWT Token 作為憑證。

這個 Token 會夾帶使用者的 idrole,後續請求時用於身分驗證。

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

Step 4:建立驗證與授權中介層(Middleware)

這是 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。

Step 5:文章 CRUD 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;

Step 6:主伺服器整合

// 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(新增 / 編輯 / 刪除)

測試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 實作篇,
讓畫面也能根據使用者角色自動變化,實現真正完整的「全端權限系統」。


上一篇
RBAC 角色權限控管 - Day 25
下一篇
RBAC 角色權限控管(下)— React + Chakra UI 權限控制 - Day27
系列文
現在就學Node.js29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言