RBAC 一篇文章 带你学会

1.什么是 RBAC?

        基于角色的访问控制(Role-Based Access Control,简称 RBAC)是一种访问控制机制,通过为用户分配角色来管理权限。RBAC 模型使得权限的管理更加简便高效,尤其适用于复杂的系统架构。

1.1 RBAC 重要概念

         用户 (User)、角色(Role)、权限(Permission)

  • 一个用户拥有若干角色
  • 一个角色拥有若干的权限
  • 这样就构成了 用户 - 角色 - 权限 的授权模型

RBAC 就是用户通过角色与权限进行关联。在这种模型中,用户与角色之间,角色与权限之间一般是多对多的关系

        

1.2 RBAC 有什么优势?

        RBAC 的优势包括权限管理的模块化、易维护性和安全性。它能够减少权限管理的复杂度,使开发者只需管理角色和权限的关系,而不必直接管理用户与权限的关系。

2.核心模块

用户管理
  • 用户列表
  • 添加用户
  • 编辑用户
  • 删除用户
  • 设置用户角色(可以有多个角色)
角色管理
  • 角色列表
  • 添加角色
  • 编辑角色
  • 删除角色
  • 分配用户权限(可以有多个权限)
权限管理
  • 权限列表
  • 添加权限
  • 编辑权限
  • 删除权限

3.技术栈

        实际上,你编写 RBAC 不需要指定的技术去实现。因为 RBAC 它只是一个权限管理模型,没有技术的限制,为了方便,我这里使用的是 Node.js,Express,MongoDB,mongoose,JWT。这里只是举例子,你用什么技术栈都可以(重要的是理解 RBAC 流程)

4.RBAC 结构设计

4.1 后端 API 路由设计
// 用户管理路由
router
  .get("/users", userCtrl.list) // 获取用户列表
  .post("/users", userCtrl.create) // 新增用户
  .put("/users/:id", userCtrl.update) // 修改用户
  .delete("/users/:id", userCtrl.delete) // 删除用户
  .get("/users/:id", userCtrl.one) // 获取单个用户
  .patch("/users/:id/roles", userCtrl.updateRoles); // 给用户分配角色
// 角色管理路由
router
  .get("/roles", rolesCtrl.list) // 获取角色列表
  .post("/roles", rolesCtrl.create) // 新增角色
  .put("/roles/:id", rolesCtrl.update) // 修改角色
  .delete("/roles/:id", rolesCtrl.delete) // 删除角色
  .get("/roles/:id", rolesCtrl.one) // 获取单个角色
  .patch("/roles/:id/menus", rolesCtrl.updateMenus); // 给角色分配权限

// 权限管理路由
router
  .get("/menus", menuCtrl.list) // 获取权限列表
  .post("/menus", menuCtrl.create) // 新增权限
  .put("/menus/:id", menuCtrl.update) // 修改权限
  .delete("/menus/:id", menuCtrl.delete) // 删除权限
  .get("/menus/:id", menuCtrl.one); // 获取单个权限
4.2 表结构设计

        这里拿 User 表举例子

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

const userSchema = new Schema({
  /**
   * 用户名
   */
  username: {
    type: String,
    required: true,
    unique: true,
  },
  /**
   * 密码
   */
  password: {
    type: String,
    required: true,
    select: false,
  },
  /**
   * 邮件
   */
  email: {
    type: String,
    unique: true,
  },
  /**
   * 姓名
   */
  name: {
    type: String,
    default: "",
  },
  /**
   * 角色
   */
  roles: {
    type: [Schema.Types.ObjectId],
    ref: "Role",
  },
  /**
   * 状态
   * 0:未启用
   * 1:已启用
   */
  status: {
    type: Number,
    default: 0,
  },
  //   创建时间
  createAt: {
    type: Date,
    default: Date.now,
  },
  //   更新时间
  updateAt: {
    type: Date,
    default: Date.now,
  },
  /**
   * 是否为超级管理员
   */
  isAdmin: {
    type: Boolean,
    default: false,
  },
});

module.exports = userSchema
4.3 表逻辑操作设计

        还是 User 表举例

// 用户处理模块
const { User } = require("../model");

// 获取用户列表
exports.list = async (req, res) => {
  try {
    const ret = await User.find().populate("roles");
    res.status(200).json(ret);
  } catch (err) {
    res.send(err);
  }
};

// 新增用户
exports.create = async (req, res) => {
  try {
    const ret = await new User(req.body).save();
    res.status(200).json(ret);
  } catch (err) {
    res.send(err);
  }
};

// 更新用户列表
exports.update = async (req, res) => {
  try {
    const ret = await User.findById(req.params.id);
    // 将客户端提交的数据合并到数据对象中
    Object.assign(ret, req.body);
    // 将修改之后的数据对象保存到数据库
    await ret.save();
    res.status(201).json(ret);
  } catch (err) {
    res.send(err);
  }
};
exports.delete = async (req, res) => {
  try {
    await User.findByIdAndDelete(req.params.id);
    res.status(204).end();
  } catch (err) {
    res.send(err);
  }
};
exports.one = async (req, res) => {
  try {
    const ret = await User.findById(req.params.id);
    res.status(200).json(ret);
  } catch (err) {
    res.send(err);
  }
};
exports.updateRoles = async (req, res,next) => {
  try{
    const ret = await User.findById(req.params.id);
    ret.roles = req.body.roles
    await ret.save();
    res.status(201).json(ret);
  }catch{
    next(err);
  }
};
4.4 配置 连接数据库
const mongoose = require("mongoose");

// 调用 main 函数,并在发生错误的时候捕获错误并打印到控制台
main().catch((err) => console.log(err));

// 连接数据库
async function main() {
  await mongoose.connect("mongodb://127.0.0.1:27017/rbac");
}

module.exports = {
  // 导入我们的模型,传递两个参数,第一个是模型名称,第二个是模型结构
  User: mongoose.model("User", require("./user")),
  Role: mongoose.model("Role", require("./role")),
  Menu: mongoose.model("Menu", require("./menu")),
  Product: mongoose.model("Product", require("./product")),
  Categorie: mongoose.model("Categorie", require("./categorie")),
};
4.5 获取用户权限
const { User } = require("../model");
const jwt = require("jsonwebtoken");

exports.login = async (req, res, next) => {
  try {
    // 根据用户名查询用户
    const user = await User.findOne({ username: req.body.username }).select(
      "+password"
    );
    // 如果没用用户,结束响应
    if (!user) {
      return res.status(400).json({
        msg: "用户名不存在",
      });
    }

    // console.log(req.body, user);

    if (req.body.password !== user.password) {
      return res.status(400).json({
        msg: "密码不正确",
      });
    }

    const token = jwt.sign(
      {
        userId: user._id,
      },
      "e5a10b19-d3c5-4a0c-8b35-388e8e237e27"
    );
    console.log(req.user);
    
    res.status(200).json({
      token,
      ...req.user,
      user
    });
  } catch (error) {
    next(error);
  }
};

exports.permissions = async (req, res, next) => {
  try {
    // 获取用户的权限
    const ret = await req.user.populate({
      path: "roles",
      populate: {
        path: "menus",
      },
    });
    res.status(200).json(ret);
  } catch (err) {
    next(err);
  }
};
4.6 token 验证

        如果我们对 token 有要求,可以自己编写登录的逻辑

4.7 动态路由实现

        我这里用到的是 判断用户权限 进行路由的添加实现的

import { createRouter, createWebHistory } from "vue-router";
import { useCounterStore } from "@/stores/counter";
import axios from "axios";
import { ElNotification } from "element-plus";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      // 重定向
      redirect: "/container",
    },
    {
      path: "/login",
      name: "login",
      // 按需导入
      component: () => import("../views/Login/LoginView.vue"),
    },
    {
      path: "/container",
      name: "container",
      component: () => import("../views/ContainerView.vue"),
      children: [],
    },
  ],
});
const dynamicRoutes = [
  {
    path: "/container",
    redirect: "/home",
  },
  {
    path: "/home",
    name: "home",
    component: () => import("../views/Main/home/HomeView.vue"),
    meta: { public: true },
  },
  {
    path: "/order",
    name: "order",
    component: () => import("../views/Main/order/OrderView.vue"),
    meta: { roles: ["管理员", "客服", "运营人员"] },
  },
  {
    path: "/cashier",
    name: "cashier",
    component: () => import("../views/Main/order/CashierOrderView.vue"),
    meta: { roles: ["管理员", "客服"] },
  },
  {
    path: "/admin",
    name: "admin",
    component: () => import("../views/Main/set/AdminView.vue"),
    meta: { roles: ["管理员"] },
  },
  {
    path: "/system",
    name: "system",
    component: () => import("../views/Main/set/SystemView.vue"),
    meta: { roles: ["管理员"] },
  },
  {
    path: "/trade",
    name: "trade",
    component: () => import("../views/Main/trade/TradeView.vue"),
    meta: { roles: ["管理员", "商品管理者"] },
  },
  {
    path: "/TradeClass",
    name: "TradeClass",
    component: () => import("../views/Main/trade/TradeClassView.vue"),
    meta: { roles: ["管理员", "商品管理者"] },
  },
  {
    path: "/user",
    name: "user",
    component: () => import("../views/Main/user/UserView.vue"),
    meta: { roles: ["管理员", "运营人员"] },
  },
  {
    path: "/UserClass",
    name: "UserClass",
    component: () => import("../views/Main/user/UserClass.vue"),
    meta: { roles: ["管理员", "运营人员"] },
  },
];
router.beforeEach(async (to, from, next) => {
  const counterStore = useCounterStore();
  const token = counterStore.token;
  const userRoles = Array.from(counterStore.userRoles);

  if (to.path !== "/login") {
    if (!token) {
      ElNotification({
        title: "提醒",
        message: "您还没有登录,请先登录",
        type: "warning",
      });

      return next({ path: "/login" });
    }

    try {
      // console.log("我验证过了");
      const as = await axios.get("/validata-token", {
        headers: {
          Authentication: `Bearer ${token}`,
        },
      });

      dynamicRoutes.forEach((route) => {
        if (route.name) {
          const existingRoute = router
            .getRoutes()
            .find((r) => r.name === route.name);
          if (existingRoute) {
            router.removeRoute(route.name);
          }
        }
      });

      // 获取用户角色信息
      const roles: string[] = userRoles ?? [];
      const accessibleRoutes = dynamicRoutes.filter((route) => {
        // 打印是否包含在路由中
        return (
          !route.meta?.roles ||
          route.meta.roles.some((role: string) => roles.includes(role))
        );
      });
      // 将符合规则的路由 添加到 container 路由
      accessibleRoutes.forEach((route) => {
        router.addRoute("container", route);
        // console.log(`添加路由:${route.name},路径:${route.path}`);
        // 打印添加的路由
      });

       // 检查目标路由是否存在于已添加的动态路由中
       const routeExists = router.getRoutes().some(route => route.path === to.path);
       if (!routeExists) {
         ElNotification({
           title: "权限提醒",
           message: "您没有权限访问该页面",
           type: "warning",
         });
         return next(false); // 阻止路由跳转
       }

      next(); // 如果是有效的token,必须访问目标路由
    } catch (error) {
      ElNotification({
        title: "提醒",
        message: "您的token已经过期,请重新登录",
        type: "warning",
      });
      console.log("error", error);
      counterStore.setToken(""); //清空无效的token
      counterStore.setUser(null);
      dynamicRoutes.forEach((route) => {
        if (route.name) {
          router.removeRoute(route.name);
        }
      });
      next({ path: "/login" }); // 跳转到登录页面
    }
  } else {
    next();
  }
});

export default router;
export { dynamicRoutes };

4.8 按钮级别

        这里就是根据我们提供的权限自行添加判断(后续会更新)

5.使用场景

        RBAC 在多用户的企业级应用、电商平台、金融系统等场景下应用广泛,特别是在系统权限需要多层级、精细化管理的情况下。对于快速发展的业务需求,RBAC 可以通过增加角色或权限来灵活应对。

6.总结

        RBAC 模型在权限管理中极大地简化了权限分配的复杂性和维护成本,尤其在角色权限关系复杂的系统中尤为适用。通过用户、角色、权限的分层管理,RBAC 实现了灵活、模块化的权限控制,使得系统在安全性和管理便捷性上都得到了提升。此外,RBAC 的可扩展性也较高,可以根据系统需求进一步扩展角色或权限的粒度,灵活适应不同的业务场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值