Xây dựng một hệ thống Role-Based Access System đơn giản sử dụng Node.js


Trong bất kỳ hệ thống nào – từ nhỏ đến lớn – đều cần giới hạn quyền truy cập. Bài viết này sẽ hướng dẫn bạn cách xây dựng một hệ thống RBAC (Role-Based Access Control) đơn giản trên server Node.js.
🔎 Kiến thức cần có trước khi bắt đầu
Hiểu cơ bản về JavaScript và Node.js
Biết cách tạo server với Express
Sử dụng cơ bản Mongoose để làm việc với MongoDB
Làm việc với Postman để test API
📌 Role-Based Access Control là gì?
RBAC là cơ chế phân quyền dựa trên vai trò. Từng người dùng được gán một vai trò (role), và hệ thống sẽ kiểm tra vai trò đó để quyết định người dùng có quyền làm gì.
Ví dụ:
Vai trò | Quyền |
Reader | Chỉ đọc bài viết |
Writer | Tạo, chỉnh sửa, xóa bài viết |
Admin | Thêm hoặc xóa Writer |
⚙️ Cách hoạt động của hệ thống RBAC
Khi người dùng đăng ký, hệ thống gán vai trò cho họ.
Thông tin người dùng và vai trò được lưu trong database.
Khi người dùng truy cập route được bảo vệ, middleware sẽ lấy role ra từ DB.
Nếu role hợp lệ với route đó, cho truy cập; ngược lại từ chối.
✅ Ưu điểm của hệ thống RBAC
Tăng bảo mật: Giới hạn quyền truy cập theo vai trò.
Hạn chế tấn công: Không ai có toàn quyền – nếu bị hack cũng không ảnh hưởng toàn hệ thống.
Đơn giản hóa quản lý: Không cần cấp nhiều mật khẩu cho từng route.
Rõ ràng trách nhiệm: Dễ truy vết ai đã làm gì.
❌ Nhược điểm
Bùng nổ vai trò: Tạo quá nhiều vai trò nếu không kiểm soát tốt.
Xung đột vai trò: Một người có thể bị gán các role gây xung đột quyền (ví dụ: tạo và duyệt cùng một đơn hàng).
🧠 Best Practices khi triển khai RBAC
Xác định rõ dữ liệu/route cần bảo vệ.
Phân nhóm người dùng theo vai trò rõ ràng.
Tránh tạo quá nhiều vai trò không cần thiết.
Vai trò nên áp dụng cho nhóm người dùng, không phải cá nhân đơn lẻ.
Xây dựng cơ chế thêm/sửa/xóa vai trò dễ bảo trì.
RBAC là thứ cần liên tục cải tiến theo thời gian.
🛠️ Bắt đầu xây dựng hệ thống
1. Khởi tạo project
mkdir rbac-system
cd rbac-system
npm init -y
2. Cài đặt package cần thiết
npm install bcrypt body-parser cookie-parser cors dotenv express jsonwebtoken mongoose nodemon
3. Cài đặt mongo database
Trong bài viết này tôi sẽ sử dụng docker để tạo server mongodb sau đây là file docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:latest
container_name: rbac-mongodb
ports:
- "${MONGO_PORT}:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
- MONGO_INITDB_DATABASE=${MONGO_DATABASE}
volumes:
- mongodb_data:/data/db
networks:
- rbac-network
networks:
rbac-network:
driver: bridge
volumes:
mongodb_data:
Trong docker-compose.yml:
Sử dụng cú pháp
${VARIABLE_NAME}
để tham chiếu đến biến môi trườngDocker Compose tự động đọc file
.env
trong cùng thư mụcĐây là ví dụ file
.env
# MongoDB Configuration MONGO_USERNAME=rbac_user MONGO_PASSWORD=rbac_password MONGO_DATABASE=rbac_db_1 MONGO_PORT=27017 # Node.js Server Configuration SERVER_PORT=3000 NODE_ENV=development APP_SECRET=a75638e528d0a947012c61222e0c8354
Sử dụng các câu lệnh sau để start/stop container mongodb
# start
docker-compose up -d
#stop
docker compose down -v
Để kiểm tra xem server mongodb đã chạy chưa sử dụng bạn hãy sử dụng lệnh sau:
docker exec -it rbac-mongodb mongosh -u rbac_user -p rbac_password --authenticationDatabase admin
Kết quả như sau:
4. Tạo schema cho nhân viên (Employee)
const { Schema, model } = require('mongoose');
const EmployeeSchema = new Schema(
{
name: {
type: String,
require: true
},
email: {
type: String,
require: true,
},
role: {
type: String,
enum: ["se", "marketer", "HR", "admin"]
},
password: {
type: String,
require: true,
}
},
{ timestamps: true }
);
module.exports = model("employee", EmployeeSchema);
5. Đăng ký người dùng (Signup)
Xử lý:
Kiểm tra trùng tên hoặc email
Hash password
Lưu vào database
const bcrypt = require('bcrypt');
const Employee = require("../Database/employee");
/**
* @DESC To register the employee (ENGINEER, MARKETER, HR-PERSONNEL)
*/
const employeeSignup = async (req, role, res) => {
try {
// validate the name
const nameNotTaken = await validateEmployeename(req.name);
if (!nameNotTaken) {
return res.status(400).json({
message: 'Employee is already registered.'
})
}
// validate the email
const emailNotRegistered = await validateEmail(req.email);
if (!emailNotRegistered) {
return res.status(400).json({
message: 'Email is already registered.'
})
}
// get the hashed password
const password = await bcrypt.hash(req.password, 12);
// create a new user
const newEmployee = new Employee({
...req,
password,
role
})
await newEmployee.save();
return res.status(201).json({
message: "Hurry! now you are successfully registred. Please nor login."
});
} catch (error) {
return res.status(500).json({
message: `${err.message}`
});
}
}
6. Đăng nhập người dùng (Login)
Xử lý:
Kiểm tra email tồn tại
Kiểm tra đăng nhập đúng portal (route theo role)
So sánh password
Trả về JWT nếu hợp lệ
/**
* @DESC To Login the employee (ENGINEER, MARKETER, HR-PERSONNEL)
*/
const employeeLogin = async (req, role, res) => {
let { email, password } = req;
console.log('role', role);
// check if the email is in the database
const employee = await Employee.findOne({ email });
if (!employee) {
return res.status(404).json({
message: "Employee email is not found. Invalid login credentials.",
})
};
// check the role
if (employee.role !== role) {
return res.status(404).json({
message: "Please make sure you are logging in from the right portal.",
})
};
// check for the password
const isMatch = await bcrypt.compare(password, employee.password);
if (isMatch) {
// sign the token and issue it to the user
const token = jwt.sign({
id: employee.id,
role: employee.role,
name: employee.name,
email: employee.email
},
process.env.APP_SECRET,
{ expiresIn: "1 days" }
)
let result = {
id: employee.id,
name: employee.name,
role: employee.role,
email: employee.email
}
res.status(200).cookie('jwt', token, {
expires: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000),
secure: false,
httpOnly: true
})
return res.json({
...result,
message: "You are now logged in."
})
} else {
return res.status(403).json({
message: "Incorrect username or password."
})
}
}
7. Middleware kiểm tra token & vai trò
/**
* @DESC Verify JWT Middleware
*/
const employeeAuth = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(403).json({
message: 'Missing authorization header.'
});
}
const token = authHeader.split(' ')[1];
jwt.verify(
token,
process.env.APP_SECRET,
(err, decoded) => {
if (err) return res.status(404).json({
message: 'invalid token'
}); //invalid token
req.auth = decoded;
next();
}
)
}
/**
* @DESC Check Role Middleware
*/
const checkRole = roles => async (req, res, next) => {
const email = req.auth?.email;
const employee = await Employee.findOne({ email });
!roles.includes(employee.role) ? res.status(401).json("Sorry you do not have access to this route") : next();
}
Lấy token từ
Authorization
header (định dạngBearer <token>
).Nếu không có header → trả về lỗi 403.
Dùng
jwt.verify()
để kiểm tra token vớiprocess.env.APP
_SECRET
.Nếu token không hợp lệ → trả về lỗi 404.
Nếu hợp lệ → gắn thông tin người dùng đã decode vào
req.auth
.Gọi
next()
để tiếp tục sang middleware tiếp theo.
8. Định nghĩa route
Tạo các route như:
/signup/se
,/signup/marketer
,/signup/hr
/login/se
,/login/marketer
,/login/hr
/se-protected
,/marketers-protected
,/hr-protected
Các route riêng được bảo vệ bằng employeeAuth
và checkRole(["role"])
9. Hoàn thiện
Dưới đây là code hoàn thiện của file server.js
require('dotenv').config()
const express = require('express');
const bp = require('body-parser');
const cookieParser = require('cookie-parser')
const mongoose = require('mongoose');
const cors = require('cors');
const userRouter = require('./routes/userRouter')
const app = express();
const PORT = process.env.PORT || 3000;
//Connecting our database
const MONGODB_URI = `mongodb://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@127.0.0.1:${process.env.MONGO_PORT}/${process.env.MONGO_DATABASE}?authSource=admin`
console.log('MONGODB_URI', MONGODB_URI)
mongoose.connect(MONGODB_URI)
.then(() => {
console.log('MongoDB connected...');
})
app.use(bp.json());
app.use(cookieParser());
app.use(bp.urlencoded({ extended: true }))
const corsOptions = {
origin: 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'Access-Control-Allow-Credentials']
};
app.use(cors(corsOptions));
app.use('', userRouter);
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});
Dưới đây là file userRouter.js
const router = require('express').Router();
const {
checkRole,
employeeAuth,
employeeLogin,
employeeSignup,
jwtAuth
} = require('../controllers/authFunctions');
// Software engineering Registeration Route
router.post("/register-se", (req, res) => {
employeeSignup(req.body, "se", res);
})
//Marketer Registration Route
router.post("/register-marketer", async (req, res) => {
await employeeSignup(req.body, "marketer", res);
});
//Human resource Registration route
router.post("/register-hr", async (req, res) => {
await employeeSignup(req.body, "hr", res);
});
// Software engineers Login Route
router.post("/login-se", async (req, res) => {
await employeeLogin(req.body, "se", res);
});
// Human Resource Login Route
router.post("/login-hr", async (req, res) => {
await employeeLogin(req.body, "hr", res);
});
// Marketer Login Route
router.post("/login-marketer", async (req, res) => {
await employeeLogin(req.body, "marketer", res);
});
//Software engineers protected route
router.get(
"/se-protected",
employeeAuth,
checkRole(["se"]),
async (req, res) => {
return res.json({ message: `welcome ${req.auth.role} - ${req.auth.name}` });
}
);
//Marketers protected route
router.get(
"/marketers-protected",
employeeAuth,
checkRole(["marketer"]),
async (req, res) => {
return res.json({ message: `welcome ${req.auth.role} - ${req.auth.name}` });
}
);
//HR personels protected route
router.get(
"/hr-protected",
employeeAuth,
checkRole(["hr"]),
async (req, res) => {
return res.json({ message: `welcome ${req.auth.role} - ${req.auth.name}` });
}
);
router.post("/protected", jwtAuth, (req, res) => {
res.status(200).send("Here's the info you requested ");
});
module.exports = router;
🔐Testing app của chúng ta
Đăng ký người dùng
curl --location 'localhost:3000/register-se' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "eminel",
"email": "eminel@gmail.com",
"password": "abc@123456"
}'
curl --location 'localhost:3000/register-marketer' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "marketer 1",
"email": "marketer1@gmail.com",
"password": "abc@123456"
}'
curl --location 'localhost:3000/register-hr' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "hr 1",
"email": "hr1@gmail.com",
"password": "abc@123456"
}'
Login người dùng
curl --location 'localhost:3000/login-se' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "eminel@gmail.com",
"password": "abc@123456"
}'
curl --location 'localhost:3000/login-hr' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "hr1@gmail.com",
"password": "abc@123456"
}'
curl --location 'localhost:3000/login-marketer' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "marketer1@gmail.com",
"password": "abc@123456"
}'
Truy cập các protected
router
curl --location 'localhost:3000/se-protected' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4MjMwMjIxYTg1MmIzYmRjZmIyY2Y2ZCIsInJvbGUiOiJzZSIsIm5hbWUiOiJlbWluZWwiLCJlbWFpbCI6ImVtaW5lbEBnbWFpbC5jb20iLCJpYXQiOjE3NDcxMjc0MjUsImV4cCI6MTc0NzIxMzgyNX0.nMgFOcEwYlfc9qRdnPQYaQsSSWt7GlciTMEbEPi77Pg' \
--header 'Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4MjMwMzE5YTg1MmIzYmRjZmIyY2Y3MSIsInJvbGUiOiJtYXJrZXRlciIsIm5hbWUiOiJtYXJrZXRlciAxIiwiZW1haWwiOiJtYXJrZXRlcjFAZ21haWwuY29tIiwiaWF0IjoxNzQ3MTI5MDgwLCJleHAiOjE3NDcyMTU0ODB9.Trp3BLJQ4rgFMPtriAr9Ry9vePh4ECQT2-rnYaGgHcE'
Nếu bạn sử dụng token của user khác truy cập thì sẽ nhận thông báo như sau
🔚 Kết luận
RBAC giúp hệ thống của bạn an toàn, rõ ràng và dễ quản lý khi có nhiều người dùng và vai trò khác nhau. Hãy áp dụng theo hướng modular và dễ mở rộng để quản lý lâu dài hiệu quả hơn.
Tải source tại đây
Subscribe to my newsletter
Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Eminel
Eminel
Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.