为了账号安全,请及时绑定邮箱和手机立即绑定

Node.js中的基于角色的访问控制(RBAC)详解

大家好!今天我们来聊聊Node.js里的RBAC(基于角色访问控制)。

这个故事包含:

  • RBAC(有真实生活例子的定义,而不是那种枯燥的定义)
  • 包含源代码的示例项目

开始之前,你可以通过下方链接查阅该项目源代码。

GitHub - carpodok/medium-stories: 以下是在 Medium 上详细介绍的项目。通过提交新内容来参与更新……github.com

你可能还会对以下代码库感兴趣:这是一个医院应用程序的API,使用基于角色的访问控制(RBAC)和MongoDB进行数据库管理。

GitHub - carpodok/node-rbac-healthcare-system-app: 一个使用 Node.js 构建的项目,实现了针对医疗系统的基于角色的访问控制 (RBAC),比如定义了管理员等角色……在 github.com 上 角色权限 (RBAC)

参考

基于角色的访问控制(RBAC)基本上是一种允许我们在项目中执行某些限制的方式。换句话说,它是一种安全模型,通过给用户分配角色,帮助你以有序的方式管理用户权限。不是直接将权限授予每个用户,你将权限分组到角色中,然后将这些角色分配给用户。现在我们已经跳过了基于角色访问控制(RBAC)的枯燥定义,让我们通过一个现实生活中的医院例子来更好地理解它。

想象你正在运营一家医院,不同的人(医生、患者和行政人员)需要不同的访问权限。医生可以更新患者的记录,但患者只能查看他们自己的记录。这就是RBAC的运作方式。

RBAC就像是一种安全系统,给每个人发放一个基于其角色的权限卡。用户登录时,系统会根据用户是医生、患者还是管理员来检查权限,并决定他们能访问什么和做什么。这是一种简单的方法,确保每个人都有正确的权限,从而确保所有事情既安全又高效。

RBAC 示例项目简介

这里有一个小动画 ref

在这个项目中,我们将尝试了解RBAC在实际项目中的应用。需要说明的是,该项目只涉及RBAC的基本用法,不包含高级应用。

以下是此项目的文件夹结构。

打开终端并输入这些命令来创建一个新的Node.js应用。

    mkdir rbac-example && cd rbac-example
    # 创建一个名为rbac-example的目录并进入该目录

然后,

npm init -y  # 自动初始化npm项目并回答所有问题为默认值

运行以下命令,安装所有必需的依赖。

npm i bcryptjs dotenv nodemon express jsonwebtoken

最后但并非最不关键,让我们如下配置 package.json 文件

    "scripts": {  
        "test": "echo \"错误:未指定测试选项\" && exit 1",  
        "start": "node server.js",  
        "dev": "nodemon server.js"  
    },

这里我们设定初始化的方式。

看这个 GIF: https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMDR5NncwMjVjYzd3dmJ2b2w1a3dkbG9lOXhxeXdwa3M4OTZ1NmlxeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/BpGWitbFZflfSUYuZ9/giphy.gif

现在我们准备好了,现在我们可以出发了!先来创建 server.js 文件。

    const express = require("express");  
    const dotenv = require("dotenv");  

    dotenv.config();  
    const app = express();  
    app.use(express.json());  

    app.use("/", (req, res) => {  
      res.send("你好 RBAC 示例!");  
    });  

    const PORT = process.env.PORT || 5000;  

    app.listen(PORT, () => {  
      console.log(`服务器正在端口 ${PORT} 上运行`);  
    });

这是基本服务器。要检查是否有问题,请启动一下服务器。

    npm run dev

此命令用于启动开发环境。

然后在你的浏览器中打开 http://localhost:5000 地址。如果没有问题,你应该会看到类似下面的信息:,

实际上,有一个更好的工具来测试端点,比如Postman。我已经为你们准备了一个公共集合。在这个集合中,你们项目中会用到的所有端点都已经准备好了。查看这里

虚构数据

在这个项目中,我们将使用模拟数据来测试路由。我将使用以下 data.js 文件并从这个文件中获取数据。这里有两种类型的数据:一种是用户及其角色信息,另一种是需要操作的资源。

我们可以在utils文件夹下创建文件。


    const users = [  
      { id: 1, username: "admin", password: "adminpass", role: "Admin" },  
      { id: 2, username: "editor", password: "editorpass", role: "Editor" },  
      { id: 3, username: "viewer", password: "viewerpass", role: "Viewer" },  
    ];  

    const resources = [  
      { id: 1, name: "资源项 1" },  
      { id: 2, name: "资源项 2" },  
      { id: 3, name: "资源项 3" },  
    ];  

    module.exports = { users, resources };
登录路由接口

让我们来创建我们的第一个路由文件 auth.route.js。因为我们已经有了用户数据,所以这个项目不需要注册路由。

    const express = require("express");  
    const jwt = require("jsonwebtoken");  
    const { users } = require("../utils/data");  

    const router = express.Router();  

    router.post("/login", (req, res) => {  
      const { username, password } = req.body;  

      const user = users.find(  
        (u) => u.username === username && u.password === password  
      );  
      if (!user) {  
        return res.status(404).json({ msg: "账号或密码无效" });  
      }  

      const payload = { id: user.id, role: user.role };  
      const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" });  

      return res.json({ token });  
    });  

    module.exports = router;

我们定义了一个登录接口,用于验证提供的 usernamepassword 是否存在于系统中。如果验证通过,然后会为用户生成一个 JWT 令牌,并将其作为响应返回。此令牌通常用于对应用程序中的受保护路由进行后续请求时的身份验证。

JWT是一种用于安全传输信息的JSON对象,通常在各方之间使用。在这种情况下,它携带了用户的_id_和_role_信息,从而使得后续请求可以进行基于令牌的身份验证。接下来的章节中我们会用到它。

我们来启动服务器程序,试试看路由功能怎么样。

如果一切顺利,我们就应该收到这样的回复。

资源

现在用户登录已经准备好了,我们可以开始资源部分了。让我们开始创建一个控制器文件(例如:resource.controller.js),位于 controllers 文件夹中,如下所示:

    const { resources } = require("../utils/data"); // 引入资源数据  

    // 获取所有资源信息  
    const getResources = (req, res) => {  
      res.status(200).json(resources);  
    };  

    // 创建新资源条目  
    const createResource = (req, res) => {  
      const newResource = { id: resources.length + 1, name: req.body.name };  
      resources.push(newResource);  
      res.status(201).json({ msg: "资源已创建", resource: newResource });  
    };  

    // 更新现有资源条目  
    const updateResource = (req, res) => {  
      const resource = resources.find((r) => r.id == req.params.id);  
      if (!resource) {  
        return res.status(404).json({ msg: "资源未找到" });  
      }  
      resource.name = req.body.name;  
      res.status(200).json({ msg: "资源已更新", resource });  
    };  

    // 删除资源条目  
    const deleteResource = (req, res) => {  
      const index = resources.findIndex((r) => r.id == req.params.id);  
      if (index === -1) {  
        return res.status(404).json({ msg: "资源未找到" });  
      }  
      resources.splice(index, 1);  
      res.status(200).json({ msg: "资源已删除" });  
    };  

    module.exports = {  
      getResources,  
      createResource,  
      updateResource,  
      deleteResource,  
    };

本文件中将执行资源路由模块的 CRUD 操作,并将使用这些模块在 resource.route.js 文件中。

    const express = require("express");  
    const {  
      getResources,  
      createResource,  
      updateResource,  
      deleteResource,  
    } = require("../controllers/resource.controller");  

    const router = express.Router();  

    // 获取所有资源  
    router.get(  
      "/",  
      getResources  
    );  

    // 创建资源  
    router.post("/", createResource);  

    // 更新资源  
    router.put("/:id", updateResource);  

    // 删除资源  
    router.delete("/:id", deleteResource);  

    module.exports = router;

我们定义了资源路由,并将控制器模块分配给它们。当我们通过Postman测试这些路由时。应该看到以下响应。

如果我们得到这些回复也没问题,

我们确实收到了回应,但这对吗?不是吧!

我们为用户分配了不同的角色,因此,我们需要验证一些条件,比如,用户是否登录了,是否有权限执行特定的操作。我们将使用中间件来实现这些功能。

中间件.

简而言之,Node.js中的中间件就是处理客户端请求和服务器最终响应之间事务的小程序。就像机场安检一样,护照检查或行李扫描这样的中间件功能,就像处理乘客并决定他们是否可以登机一样,中间件处理请求并决定是否返回响应。

同样,在Node.js中,中间件可以用来验证用户、记录请求日志或检查权限状态,在发送响应前确保一切就绪,再进行下一步。

我们根据目的在middlewares文件夹下创建了auth.js中间件文件。

    const jwt = require("jsonwebtoken"); 

    // 验证令牌
    const verifyAuth = (req, res, next) => { 
      const authHeader = req.header("Authorization"); 

      if (!authHeader) { 
        return res 
          .status(401) 
          .json({ success: false, message: "未提供令牌" }); 
      } 

      const token = authHeader.replace("Bearer ", ""); 

      try { 
        const decoded = jwt.verify(token, process.env.JWT_SECRET); 
        req.user = decoded; 
        next(); 
      } catch (error) { 
        res.status(401).json({ msg: "令牌无效,请检查您的令牌是否正确" }); 
      } 
    }; 

    // 验证用户角色
    const checkRole = (roles) => { 
      return (req, res, next) => { 
        if (!roles.includes(req.user.role)) { 
          return res.status(403).json({ 
            msg: "您没有权限执行此操作!", 
          }); 
        } 
        next(); 
      }; 
    }; 

    module.exports = { verifyAuth, checkRole };

我们在这个文件中定义了两个中间件函数。

授权验证:

  • 此功能检查传入的HTTP请求是否有包含JSON Web Token(JWT)的Authorization头部。
  • 若缺少该头部,则返回401状态码并附带错误消息。
  • 如果令牌存在,则移除“Bearer ”部分,仅保留令牌部分。
  • 然后使用存储在环境变量JWT_SECRET中的密钥,通过jwt.verify方法来验证令牌。
  • 如果令牌有效,则将令牌中的解码用户数据附加到req.user,并且请求继续到下一个中间件或路由处理器(resource.controller.js)。
  • 如果令牌无效,则返回401状态码并附带错误消息,指出令牌无效。

检查角色权限

  • 此中间件函数用来根据用户角色限制访问。
  • 它接收一个允许角色的数组作为参数。
  • 它检查用户的角色(存储在 req.user.role 中,该角色是从 verifyAuth 中的 token 中解析出来的)是否包含在允许的角色中。
  • 如果用户的角色不允许,它会返回一个 403 状态码并附带错误信息。
  • 如果用户的角色被允许,它将继续执行下一个中间件或路由处理函数。

我们现在要把这些中间件添加到 resource.route.js 文件里。

    const express = require("express");  
    const { verifyAuth, checkRole } = require("../middlewares/auth");  
    const {  
      getResources,  
      createResource,  
      updateResource,  
      deleteResource,  
    } = require("../controllers/resource.controller");  

    const router = express.Router();  

    // 获取所有资源信息  
    router.get(  
      "/",  
      verifyAuth,  
      checkRole(["Admin", "Editor", "Viewer"]),  
      getResources  
    );  

    // 新增资源  
    router.post("/", verifyAuth, checkRole(["Admin", "Editor"]), createResource);  

    // 更新现有资源信息  
    router.put("/:id", verifyAuth, checkRole(["Admin", "Editor"]), updateResource);  

    // 删除资源信息  
    router.delete("/:id", verifyAuth, checkRole(["Admin"]), deleteResource);  

    module.exports = router;

在更新的 resource.route.js 文件中,我们首先检查用户是否已登录。这样,我们可以确保如果用户没登录,我们就不会白走下一步。

我们的资源访问路径有保护,所以用户必须登录后才能进行任何操作。如果用户在未登录的情况下尝试获取资源,会收到如下响应:

接下来是基于资源操作来检查角色权限。例如,要删除任何资源,必须是管理员。换句话说,非管理员尝试删除资源时,我们会拒绝。用户会收到如下错误信息:

希望这个故事能帮到大家。继续关注我,想看更多类似的内容,请点赞。

GitHub - carpodok/medium-stories: 以下是我详细描述的一些项目,发布在 Medium 上。您可以通过提交新内容来参与这个项目的开发。
GitHub - carpodok/node-rbac-healthcare-system-app: 基于 Node.js 的项目,实现了基于角色的访问控制 (RBAC) 的医疗系统,定义了管理员等角色…github.com
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
48
获赞与收藏
218

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消