How to Implement Role-Based Access Control in a Node.js REST API with JWT
TL;DR · AI 摘要
How to Implement Role-Based Access Control in a Node.js REST API with JWT July 9, 2026 / Node.js Zia Ullah The first tim...
核心要点
- 主题聚焦:How to Implement Role-Based Access Control in a
- 来源:freeCodeCamp.org,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
如何在 Node.js REST API 中使用 JWT 实现基于角色的访问控制
2026年7月9日
/
#Node.js
Zia Ullah
第一次构建 API 时没有考虑角色概念,我给每个登录用户都提供了相同的访问权限。直到普通用户意外触发删除端点并清除了测试数据,这才让我真正坐下来系统学习 RBAC。
基于角色的访问控制(RBAC)听起来很高级,但核心思想很简单:你的操作权限取决于你的身份,而不仅仅是登录状态。管理员可以删除用户,编辑者可以创建文章,普通用户只能阅读。相同的应用,根据请求者身份会呈现出完全不同的使用体验。
这就是我们即将构建的内容:一个包含三个角色的 REST API,使用 JWT 在每次请求中携带角色信息,并通过两个中间件函数在路由处理程序执行前检查权限。整个实现无需每次请求都访问数据库,业务逻辑中也不会出现复杂的 if/else 判断。
最终你将获得三个角色(admin、editor、user)各自锁定在专属端点的完整实现。更重要的是,这种模式具有可移植性:一旦理解原理,你可以在下一个项目中直接应用而无需教程指导。
完整源代码在 GitHub:github.com/ziaongit/nodejs-rbac-jwt-api
目录
- 你将学到的内容
- 先决条件
- 我们要构建的内容
- 项目初始化
- 设置内存数据存储
- 构建认证路由
- 构建 RBAC 中间件
- 构建受保护路由
- 整合所有组件
- 测试 API
- 关键收获
- 总结
你将学到的内容
- 理解 RBAC 的概念及其与基础认证的差异
- 学习如何在 JWT 负载中嵌入角色信息
- 掌握编写可复用的 Express 中间件实现令牌验证和角色检查
- 学习如何根据用户角色保护 API 路由
先决条件
- 安装 Node.js (v18+)
- 掌握 Express.js 基础知识
- 熟悉 JWT 工作原理(我们将讲解关键部分)
- 安装 npm
我们要构建的内容
我们将构建一个包含三个用户角色的简单内容管理系统 REST API:
| 角色 | 权限 | |--------|------------------------------| | user | 读取内容 | | editor | 读取 + 创建内容 | | admin | 完全访问 - 读取、创建、删除内容,管理用户 |
API 将暴露以下端点:
| 方法 | 端点 | 访问权限 | |--------|------------------|------------------| | POST | /api/auth/register | 公共 | | POST | /api/auth/login | 公共 | | GET | /api/content | user, editor, admin | | DELETE | /api/content/:id | editor, admin | | GET | /api/admin/users | admin 专用 |
项目初始化
创建新文件夹并初始化项目:
mkdir nodejs-rbac-jwt-api
cd nodejs-rbac-jwt-api
npm init -y安装依赖:
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon各依赖包作用说明:
- express:构建 API 的 Web 框架
- jsonwebtoken:创建和验证 JWT
- bcryptjs:安全地哈希密码
- dotenv:读取 .env 文件,避免在源代码中硬编码密钥
更新 package.json 添加启动脚本:
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
}创建项目结构:
nodejs-rbac-jwt-api/
├── src/
│ ├── middleware/
│ │ └── auth.js
│ ├── routes/
│ │ ├── auth.js
│ │ ├── content.js
│ │ └── admin.js
│ ├── data/
│ │ └── users.js
│ └── app.js
├── .env
├── .env.example
└── package.json创建 .env 文件:
JWT_SECRET=your_super_secret_key_change_this_in_production
PORT=3000重要:切勿将 .env 文件提交到版本控制中。将其添加到 .gitignore 文件中。
设置内存数据存储
此处我们没有使用数据库,仅使用内存中的数组。这样设计的目的是将重点放在基于角色的访问控制(RBAC)上,而不是花费一半的教程时间在数据库配置上。在实际项目中,请将数组替换为当前正在使用的数据库。
创建 src/data/users.js 文件:
// 内存用户存储
// 生产环境请替换为真实数据库(MongoDB、PostgreSQL 等)
const users = [];
const findUserByEmail = (email) => users.find((u) => u.email === email);
const findUserById = (id) => users.find((u) => u.id === id);
const createUser = (user) => {
users.push(user);
return user;
};
const getAllUsers = () => users.map(({ password, ...user }) => user);
module.exports = { findUserByEmail, findUserById, createUser, getAllUsers };需要注意的一点:getAllUsers 使用解构语法在返回结果前移除了密码字段。即使是对哈希后的密码,也绝不应在 API 响应中返回密码字段。
构建认证路由
认证路由负责处理注册和登录。登录是角色首次出现的地方——我们将用户的角色直接嵌入到 JWT 的负载中。
创建 src/routes/auth.js 文件:
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { findUserByEmail, createUser } = require('../data/users');
const router = express.Router();
// POST /api/auth/register
router.post('/register', async (req, res) => {
const { name, email, password, role } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ message: '姓名、电子邮件和密码是必填项' });
}
if (findUserByEmail(email)) {
return res.status(409).json({ message: '电子邮件已注册' });
}
// 仅允许有效角色——如果没有提供则默认为 'user'
const validRoles = ['user', 'editor', 'admin'];
const assignedRole = validRoles.includes(role) ? role : 'user';
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
id: Date.now().toString(),
name,
email,
password: hashedPassword,
role: assignedRole,
};
createUser(newUser);
res.status(201).json({
message: '用户注册成功',
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
role: newUser.role,
},
});
});
// POST /api/auth/login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: '电子邮件和密码是必填项' });
}
const user = findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: '凭证无效' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: '凭证无效' });
}
// 生成 JWT —— 将角色嵌入到负载中
const token = jwt.sign(
{
id: user.id,
email: user.email,
role: user.role, // ← 这是 RBAC 的关键部分
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
message: '登录成功',
token,
});
});
module.exports = router;最关键的一行是 JWT 的负载:
jwt.sign({ id, email, role }, process.env.JWT_SECRET, { expiresIn: '24h' })通过将角色嵌入到令牌中,后续每次请求都会携带用户的权限信息,无需进行数据库查询。服务器只需验证令牌并从负载中读取角色信息即可。
构建RBAC中间件
这是系统的核心部分。我们需要两个独立的中间件函数:
- verifyToken 用于验证JWT的有效性,并将解码后的负载附加到req.user
- checkRole 用于验证用户是否具有特定路由所需的权限
保持它们的分离性可以提供更大的灵活性。某些路由只需认证,其他路由则需要同时具备认证和特定角色权限。
创建 src/middleware/auth.js:
const jwt = require('jsonwebtoken');
// 中间件1:验证JWT令牌
const verifyToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // 期望格式:Bearer <token>
if (!token) {
return res.status(401).json({ message: '访问被拒绝。未提供令牌。' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 将解码后的负载(包含角色)附加到请求
next();
} catch (err) {
return res.status(403).json({ message: '无效或过期的令牌。' });
}
};
// 中间件2:检查用户是否具有所需角色
const checkRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: '未认证。' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
message: `访问被拒绝。所需角色:${allowedRoles.join(' 或 ')}. 您的角色:${req.user.role}`,
});
}
next();
};
};
module.exports = { verifyToken, checkRole };checkRole使用剩余参数(...allowedRoles)允许你传入一个或多个角色:
checkRole('admin') // 仅管理员
checkRole('editor', 'admin') // 编辑或管理员
checkRole('user', 'editor', 'admin') // 所有角色这使得路由定义更加清晰易读——权限信息可以直接在路由层级看到。
构建受保护的路由
现在让我们连接使用中间件的路由。
创建 src/routes/content.js:
const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const router = express.Router();
// 内存中的内容存储
const content = [
{ id: '1', title: 'Node.js入门', author: 'admin' },
{ id: '2', title: 'Express中间件详解', author: 'editor' },
];
// GET /api/content — 所有已认证用户
router.get('/', verifyToken, checkRole('user', 'editor', 'admin'), (req, res) => {
res.json({ content });
});
// POST /api/content — 仅编辑器和管理员
router.post('/', verifyToken, checkRole('editor', 'admin'), (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ message: '标题是必填项' });
}
const newItem = {
id: Date.now().toString(),
title,
author: req.user.email,
};
content.push(newItem);
res.status(201).json({ message: '内容已创建', item: newItem });
});
// DELETE /api/content/:id — 仅管理员
router.delete('/:id', verifyToken, checkRole('admin'), (req, res) => {
const index = content.findIndex((c) => c.id === req.params.id);
if (index === -1) {
return res.status(404).json({ message: '内容未找到' });
}content.splice(index, 1);
res.json({ message: 'Content deleted successfully' });
});
module.exports = router;请注意每条路由的可读性:
router.delete('/:id', verifyToken, checkRole('admin'), handler)无需阅读处理程序主体即可理解访问控制。这是基于中间件的RBAC(基于角色的访问控制)的主要优势之一:权限位于路由层,而不是隐藏在业务逻辑中。
创建 src/routes/admin.js:
const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const { getAllUsers } = require('../data/users');
const router = express.Router();
// GET /api/admin/users — 仅限管理员
router.get('/users', verifyToken, checkRole('admin'), (req, res) => {
res.json({ users: getAllUsers() });
});
module.exports = router;整合所有内容
创建 src/app.js:
require('dotenv').config();
const express = require('express');
const authRoutes = require('./routes/auth');
const contentRoutes = require('./routes/content');
const adminRoutes = require('./routes/admin');
const app = express();
app.use(express.json());
// 路由
app.use('/api/auth', authRoutes);
app.use('/api/content', contentRoutes);
app.use('/api/admin', adminRoutes);
// 健康检查
app.get('/', (req, res) => {
res.json({ message: 'RBAC API is running' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});测试API
启动服务器:
npm run dev第1步:注册具有不同角色的用户
注册管理员:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name": "Admin User", "email": "admin@example.com", "password": "password123", "role": "admin"}'注册编辑器:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name": "Editor User", "email": "editor@example.com", "password": "password123", "role": "editor"}'注册普通用户(未指定角色,默认为user):
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name": "Regular User", "email": "user@example.com", "password": "password123"}'第2步:登录并获取令牌
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password123"}'您将获得类似以下的响应:
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}复制该令牌。
第3步:测试基于角色的访问
以普通用户身份读取内容(应成功):
curl http://localhost:3000/api/content \
-H "Authorization: Bearer YOUR_TOKEN_HERE"以普通用户身份尝试创建内容(应失败 - 403):
curl -X POST http://localhost:3000/api/content \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"title": "New Article"}'响应:
{
"message": "Access denied. Required role: editor or admin. Your role: user"
}现在以编辑器身份登录并尝试相同的POST请求。操作将成功。以管理员身份登录并尝试DELETE路由。只有管理员令牌可以工作。
第4步:解码JWT查看角色
您可以将任何令牌粘贴到jwt.io中查看负载。您将看到类似以下内容:
{
"id": "1720300000000",
"email": "admin@example.com",
"role": "admin",
"iat": 1720300000,
"exp": 1720386400
}role字段正是checkRole在每次受保护的请求中读取的内容。
关键要点
角色信息存储在JWT负载中。角色信息随令牌一起传输——无需每次访问受保护路由时都进行额外的数据库查询。登录时角色信息会被嵌入令牌,并在每次请求时通过加密验证。
中间件具有组合性。verifyToken和checkRole是独立可复用的函数。你可以将它们以任何组合方式应用到任何路由上。
权限控制在路由层级可见。router.delete('/:id', verifyToken, checkRole('admin'), handler) 这行代码在查看处理函数前就已明确说明了所有访问控制规则。
在部署到生产环境前:
- 内存数组仅用于保持教程聚焦——在任何内容接近生产环境前,请用真实数据库替代。目前服务器重启会立即清除所有用户数据。
- 24小时的令牌有效期过长。缩短至15分钟并添加刷新令牌轮换机制。被盗令牌将迅速失效。
- 在敏感操作中从数据库重新验证角色。角色变更后,现有令牌需等到过期后才会反映新角色。
- 始终使用HTTPS
- 如果权限逻辑超出"检查角色"的范畴,可以考虑使用casl。它能清晰处理属性级规则
结论
核心实现仅需两个中间件函数和一个JWT负载。我曾在多个项目中使用过这种模式。一旦自己实现过,你会开始在各处发现这种模式,因为几乎所有多用户应用都需要某种形式的这种机制。
Zia Ullah是瑞典ValueAdd Solution Scandinavia AB(www.valueadd.se)的全栈开发人员,拥有软件工程硕士学位和计算机科学学士学位。他拥有13年以上的经验,曾在Azure上构建和部署应用程序,使用GitHub Actions和Azure DevOps配置CI/CD流水线,并在医疗保健、物流和SaaS项目中实施安全控制。他也是技术作家,作品发表在freeCodeCamp、DevOps.com、Dev.to和HackerNoon等平台,分享关于Web开发、云计算、DevOps和软件工程的实用见解。LinkedIn: www.linkedin.com/in/zia-ullah/
如果这篇文章对你有帮助,请分享它。
免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成为开发者。立即开始
ADVERTISEMENT