freeCodeCamp.org

How to Implement Role-Based Access Control in a Node.js REST API with JWT

7.1内容质量

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 分析暂不可用,本条为保底评分与摘要。
#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 专用 |

项目初始化

创建新文件夹并初始化项目:

code
mkdir nodejs-rbac-jwt-api
cd nodejs-rbac-jwt-api
npm init -y

安装依赖:

code
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon

各依赖包作用说明:

  • express:构建 API 的 Web 框架
  • jsonwebtoken:创建和验证 JWT
  • bcryptjs:安全地哈希密码
  • dotenv:读取 .env 文件,避免在源代码中硬编码密钥

更新 package.json 添加启动脚本:

code
"scripts": {
  "start": "node src/app.js",
  "dev": "nodemon src/app.js"
}

创建项目结构:

code
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 文件:

code
JWT_SECRET=your_super_secret_key_change_this_in_production
PORT=3000

重要:切勿将 .env 文件提交到版本控制中。将其添加到 .gitignore 文件中。

设置内存数据存储

此处我们没有使用数据库,仅使用内存中的数组。这样设计的目的是将重点放在基于角色的访问控制(RBAC)上,而不是花费一半的教程时间在数据库配置上。在实际项目中,请将数组替换为当前正在使用的数据库。

创建 src/data/users.js 文件:

code
// 内存用户存储
// 生产环境请替换为真实数据库(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 文件:

code
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 的负载:

code
jwt.sign({ id, email, role }, process.env.JWT_SECRET, { expiresIn: '24h' })

通过将角色嵌入到令牌中,后续每次请求都会携带用户的权限信息,无需进行数据库查询。服务器只需验证令牌并从负载中读取角色信息即可。

构建RBAC中间件

这是系统的核心部分。我们需要两个独立的中间件函数:

  • verifyToken 用于验证JWT的有效性,并将解码后的负载附加到req.user
  • checkRole 用于验证用户是否具有特定路由所需的权限

保持它们的分离性可以提供更大的灵活性。某些路由只需认证,其他路由则需要同时具备认证和特定角色权限。

创建 src/middleware/auth.js:

code
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)允许你传入一个或多个角色:

code
checkRole('admin')                  // 仅管理员
checkRole('editor', 'admin')        // 编辑或管理员
checkRole('user', 'editor', 'admin') // 所有角色

这使得路由定义更加清晰易读——权限信息可以直接在路由层级看到。

构建受保护的路由

现在让我们连接使用中间件的路由。

创建 src/routes/content.js:

code
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: '内容未找到' });
  }
code
content.splice(index, 1);
res.json({ message: 'Content deleted successfully' });
});

module.exports = router;

请注意每条路由的可读性:

code
router.delete('/:id', verifyToken, checkRole('admin'), handler)

无需阅读处理程序主体即可理解访问控制。这是基于中间件的RBAC(基于角色的访问控制)的主要优势之一:权限位于路由层,而不是隐藏在业务逻辑中。

创建 src/routes/admin.js:

code
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:

code
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

启动服务器:

code
npm run dev

第1步:注册具有不同角色的用户

注册管理员:

code
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"}'

注册编辑器:

code
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):

code
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步:登录并获取令牌

code
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'

您将获得类似以下的响应:

code
{
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

复制该令牌。

第3步:测试基于角色的访问

以普通用户身份读取内容(应成功):

code
curl http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

以普通用户身份尝试创建内容(应失败 - 403):

code
curl -X POST http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Article"}'

响应:

code
{
  "message": "Access denied. Required role: editor or admin. Your role: user"
}

现在以编辑器身份登录并尝试相同的POST请求。操作将成功。以管理员身份登录并尝试DELETE路由。只有管理员令牌可以工作。

第4步:解码JWT查看角色

您可以将任何令牌粘贴到jwt.io中查看负载。您将看到类似以下内容:

code
{
  "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