Node.js中的基于角色的访问系统
#node #access

多次,各种规模的组织都要求Web开发人员限制对某些资源的访问权限,并根据系统中用户的层次结构实施某些效果的权利。在本文中,我们将研究如何在Our Node.js服务器中实现基于角色的访问系统。

要遵循本文的其余部分,您需要以下内容:

  • JavaScript的工作知识
  • 对node.js的良好理解以及如何使用它来创建服务器
  • 使用猫鼬对数据库创建的理解
  • 邮递员和如何使用Postman的知识

在我们继续之前,让我们解释一些概念。

什么是基于角色的访问系统?

基于角色的访问控制(RBAC)是一种安全方法,它限制了网络访问并根据组织在组织中的角色分配权限。

基于角色的访问系统的一个简单示例是一个博客,该博客允许用户在写作应用程序中创建,编辑,读取或删除文章。对于此博客,我们可以执行三个角色:

  1. 读者
  2. 作家
  3. 管理员

读者只能阅读文章,作者有权创建,编辑,删除和阅读文章,并且管理员可以添加或删除作者。有了基于角色的系统,读者将无法访问作者的角色,而作者将无法扮演管理员的角色。

基于角色的访问系统如何工作?

基于角色的访问系统依赖于具有指定角色的系统中的每个用户或实体。这个角色决定了他们的权限。这是其工作原理:

  • 当用户创建帐户时,将根据其组分配角色。然后将此角色与数据库中的其他信息一起存储。
  • 当用户尝试访问受保护的路由时,我们的中间件从数据库中检索用户的信息。
  • 用户的角色进行了交叉检查,以确认用户的角色是否匹配所需的信息以访问所请求的信息。
  • 如果用户的角色与所需的角色匹配,则授予访问权限。如果没有,访问将被拒绝。

基于角色的访问系统的优势

以下是使用基于角色的访问系统的好处:

  • 安全:加上适当的身份验证流程,RBAC可增强整体安全性,因为它与隐私,机密性和对资源以及其他敏感数据和系统的访问管理有关。
  • 降低了对网络攻击的敏感性:由于不同的群体具有不同的角色,而且没有人唯一控制该系统,因此,单个帐户上的网络攻击较少对系统造成重大伤害。
  • 降低不必要的客户支持:在某些系统中,多个密码被分配给用户的不同路线和端点。分配给用户的密码越多,他们就越有可能忘记它们。基于角色的访问控制将消除对多个密码的需求,而是根据分配给用户的初始角色授予访问权限。
  • 建立组织结构: RBAC使区分哪个用户负责每个任务。这使得知道谁做了什么并发现信息泄漏或网络问题的罪魁祸首。

基于角色的访问系统的缺点

尽管基于角色的访问系统具有许多优势,但该系统仍然存在某些缺点。其中一些是:

  • 角色爆炸:当新工人或团队被登上并且没有正确概述其职责时,可能会创建更多的角色。同样,当来自另一组的用户需要从另一组访问信息时,将分配给该用户的新角色。加入许多角色使得很难跟踪谁可以使用什么,使角色结构变得越来越复杂并损害了系统的有效性。
  • 相互冲突的组合:分配给不同用户的不同角色可能包含冲突的访问。例如,可以赋予用户一个角色,使他们能够创建订单和批准相同订单所需的角色。这可能会造成业务威胁。

实施基于角色的访问系统的最佳实践

构建基于角色的访问系统时,需要考虑某些事情和要采取的行动来维持系统并减少混乱。其中一些是:

  • 定义应限制访问的数据和资源。
  • 根据其角色和所需访问某些信息的访问权分类用户分为不同的组。应清理任何不必要的例外。
  • 避免创造太多角色。创造太多角色会破坏系统的目的,并可能导致角色爆炸。
  • 使角色可重复使用。如果系统中只有一个用户具有特定的角色,则该角色不应由基于角色的系统管理。所有定义的角色都应适用于人群,否则,您将拥有太多角色。
  • 分析在必要时如何更改角色,如何注册新用户以及如何从组中删除旧帐户。
  • 不断适应。基于角色的系统的第一次迭代将需要一些更改,因此应不断检查系统并适应涵盖不断增长的组织。

构建我们的Node.js Web服务器

为了更好地理解,我们将为拥有三个部门的公司构建服务器:

  1. 软件工程部
  2. 市场部
  3. 人力资源部

要构建我们的服务器,我们将执行以下操作:

  • 首先,我们将为服务器创建一个目录。导航到合适的目录并在您的终端中运行以下代码:
mkdir Company-Server
  • 创建我们的目录后,我们将导航到此目录并初始化NPM:
npm init

安装所需的软件包

对于此项目,我们将使用以下依赖项和软件包:

  • dotenv:此软件包将ENV文件的环境变量加载到Node的Process.env对象。
  • bcrypt 将其发送到数据库之前,用于哈希密码和其他敏感信息,以保护我们免受数据库的违反。
  • body-parser:这用于从请求主体解析传入的数据,并将解析的值附加到可以通过Express Middleware访问的对象。
  • jsonwebtoken:这提供了一种代表两方之间转移索赔的方法,以确保未经授权的第三方未对传递的信息篡改。
  • Express.js:这使我们提供了有用的功能,例如路由,实施中间件等,这使其毫不费力地构建了API和服务器端应用程序。
  • Mongoose:帮助我们与数据库联系,并提供诸如架构验证,管理数据之间的关系等功能。
npm i jsonwebtoken mongoose bcrpyt body-parser express dotenv

设置我们的数据库

对于我们的数据库,我们将使用Mongo Atlas数据库。您可以创建一个帐户,并通过遵循以下步骤轻松将其链接到您的Express服务器:

要创建我们的员工模式,请复制以下代码:

const { Schema, model } = require("mongoose");

const EmployeeSchema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
    },
    role: {
      type: String,
      enum: ["se", "marketer", "HR", "admin"],
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

module.exports = model("Employee", EmployeeSchema);

设置用户身份验证

在基于角色的访问系统检查用户角色之前,我们需要设置一条路由以使员工进入系统。之后,我们将根据其角色授予他们访问某些资源的访问。

我们将设置用于用户注册,登录和身份验证的逻辑。让我们从注册开始。

用户注册

对于我们的注册端点,我们将执行以下操作:

  • 从Frontend请求接收用户的信息
  • 哈希密码
  • 将信息发送到我们的数据库
  • 将员工重定向到登录路线
const bycrypt = require('brypt');
const Employee = require("../Database/employee");

const employeeSignup = async (req, role, res) => {
  try {
    //Get employee from database with same name if any
    const validateEmployeename = async (name) => {
      let employee = await Employee.findOne({ name });
      return employee ? false : true;
    };

    //Get employee from database with same email if any
    const validateEmail = async (email) => {
      let employee = await Employee.findOne({ email });
      return employee ? false : true;
    };
    // Validate the name
    let nameNotTaken = await validateEmployeename(req.name);
    if (!nameNotTaken) {
      return res.status(400).json({
        message: `Employee name is already taken.`,
      });
    }

    // validate the email
    let emailNotRegistered = await validateEmail(req.email);
    if (!emailNotRegistered) {
      return res.status(400).json({
        message: `Email is already registered.`,
      });
    }

// Hash password using bcrypt
    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 (err) {
    // Implement logger function if any
    return res.status(500).json({
      message: `${err.message}`
    });
  }
};

完成此操作,我们已经设置了注册逻辑。让我们设置登录逻辑。

用户登录

每个想要登录的员工都必须从为他的部门设计的路线上登录。例如,如果软件工程师试图通过营销部门的登录路线登录系统,则将拒绝访问。

对于我们的登录路线,我们将执行以下操作:

  • 从前端请求接收员工的信息
  • 验证员工是否存在于我们的数据库中
  • 检查员工是否正在通过其部门的正确路线登录
  • 如果用户通过其部门的路线登录,我们将检查密码是否正确
  • 如果密码正确,则将将用户信息与JWT令牌一起发送到客户端
const jwt = require("jsonwebtoken");
require('dotenv').config();
const Employee = require("../Database/employee");

const employeeLogin = async (req, role, res) => {
  let { name, password } = req;

  // First Check if the user exist in the database
  const employee = await Employee.findOne({ name });
  if (!employee) {
    return res.status(404).json({
      message: "Employee name is not found. Invalid login credentials.",
      success: false,
    });
  }
  // We will check the if the employee is logging in via the route for his departemnt
  if (employee.role !== role) {
    return res.status(403).json({
      message: "Please make sure you are logging in from the right portal.",
      success: false,
    });
  }

  // That means the employee is existing and trying to signin fro the right portal
  // Now check if the password match
  let isMatch = await bcrypt.compare(password, employee.password);
  if (isMatch) {
    // if the password match Sign a the token and issue it to the employee
    let token = jwt.sign(
      {
        role: employee.role,
        name: employee.name,
        email: employee.email,
      },
      process.env.APP_SECRET,
      { expiresIn: "3 days" }
    );

    let result = {
      name: employee.name,
      role: employee.role,
      email: employee.email,
      token: `Bearer ${token}`,
      expiresIn: 168,
    };

    return res.status(200).json({
      ...result,
      message: "You are now logged in.",
    });
  } else {
    return res.status(403).json({
      message: "Incorrect password.",
    });
  }
};

将基于角色的访问系统添加到服务器。

每个登录的用户都有A JWT令牌;我们将创建A middleware检查令牌。令牌的存在表明用户已登录。此中间件也将 verify the令牌。

我们还将创建另一个中间件,以限制仅具有特定角色用户对某些路由的访问。

/**
 * @DESC Verify JWT from authorization header Middleware
 */
const employeeAuth = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  console.log(process.env.APP_SECRET);
  if (!authHeader) return res.sendStatus(403);
  console.log(authHeader); // Bearer token
  const token = authHeader.split(" ")[1];
  jwt.verify(token, process.env.APP_SECRET, (err, decoded) => {
    console.log("verifying");
    if (err) return res.sendStatus(403); //invalid token

    console.log(decoded); //for correct token
    next();
  });
};

/**
 * @DESC Check Role Middleware
 */
const checkRole = (roles) => async (req, res, next) => {
  let { name } = req.body;

  //retrieve employee info from DB
  const employee = await Employee.findOne({ name });
  !roles.includes(employee.role)
    ? res.status(401).json("Sorry you do not have access to this route")
    : next();
};

员工身份验证功能检查是否存在JWT。如果找到了,它将检查是否正确。

checkrole函数检查用户请求访问是否具有访问该路线的所需角色。

设置我们的路线

在本节中,我们将创建以下路线并应用所需的中间件。

  • 每个部门的注册路线
  • 每个部门的登录路线
  • 每个部门受保护的路线
// Software engineering Registeration Route
app.post("/register-se", (req, res) => {
  employeeSignup(req.body, "se", res);
});

//Marketer Registration Route
app.post("/register-marketer", async (req, res) => {
  await employeeSignup(req.body, "marketer", res);
});

//Human resource Registration route
app.post("/register-hr", async (req, res) => {
  await employeeSignup(req.body, "hr", res);
});

// Software engineers Login Route
app.post("/Login-se", async (req, res) => {
  await employeeLogin(req.body, "se", res);
});

// Human Resource Login Route
app.post("/Login-hr", async (req, res) => {
  await employeeLogin(req.body, "hr", res);
});

// Marketer Login Route
app.post("/Login-marketer", async (req, res) => {
  await employeeLogin(req.body, "marketer", res);
});

app.get("/se-protected", employeeAuth, checkRole(["se"]), async (req, res) => {
 return res.json(`welcome ${req.body.name}`);
});

app.get(
  "/marketers-protected",
  employeeAuth,
  checkRole(["marketer"]),
  async (req, res) => {
    return res.json(`welcome ${req.body.name}`);
  }
);

app.get("/hr-protected", employeeAuth, checkRole(["hr"]), async (req, res) => {
  return res.json(`welcome ${req.body.name}`);
});

测试我们的应用程序

要测试我们的应用程序,我们将创建一个名为Victor的演示用户,并具有软件工程角色。

使用我们创建的用户,让我们尝试登录。

我们的用户登录正确!现在,让我们尝试从人力资源部的路线登录。

我们可以看到我们的用户无法通过另一个部门的路线登录。成功!

现在,让我们尝试访问受保护的路线。

我们的用户可以访问软件工程保护的路线,因为该角色已分配给他。让我们尝试使用我们的软件工程用户访问人力资源路线。

从上面的图像中,我们可以看到我们的所有路线都按预期工作。可以对它们进行调整,并且可以使用相同的逻辑添加更多路线,但是我将其留给您。

结论

在本文中,我们讨论了基于角色的访问系统,其好处及其缺点。我们还研究了如何在Node.js中实现基于角色的访问系统。愉快的编码!

资源

Repo
RBAC
JWT