首页 > 其他分享 >浅谈 RBAC 权限系统设计

浅谈 RBAC 权限系统设计

时间:2023-11-17 17:11:36浏览次数:37  
标签:菜单 浅谈 RBAC path import 权限 true 路由

方案设计

在实际业务中,权限系统的设计其实可以做到很复杂,但是为了简单起见只保留一些最基本且核心的模块:

  • 登录模块:权限平台一般需要靠登录获取用户身份,并通过凭证去请求接口,包括注册功能。
  • 系统管理模块:包括用户管理、角色管理、菜单管理(如果菜单是前端控制则可以省略)等功能,是权限系统中的核心部分。

而权限控制其实分为了两部分:数据权限功能权限 控制。主要从四个方面入手:

  • 接口权限:权限系统的最后一道保护屏障,每个权限接口都需要 token
  • 路由权限:通过拦截器阻止越权页面访问,防止用户直接通过 URL 进入页面。
  • 菜单权限:通过控制菜单权限来给不同角色的用户展示不同菜单。
  • 按钮权限:控制数据的操作,没有权限时隐藏或者按钮置灰禁用。

image.png

可以看到权限无非就是对用户的操作和视图控制,一般需要前后端人共同去配合去做,后端小伙伴更像是守门员,守住最后一道防线,而前端小伙伴则负责在球进门之前给他阻挡掉,从而减少守门员的压力。

菜单路由权限方案

有一个很重要的问题那就是,菜单和路由要怎么管理呢?有的人说放到前端管理,有的人说放到后端管理,看看两种常见菜单路由方案(下面以 Vue 生态为例)。

方案一:前端管理菜单路由

这种方案主要以 vue-element-admin 这个开源项目为代表的方案:

  • 设置两种路由:公共路由权限路由。同时在路由表的 meta 字段中绑定菜单相关信息。
  • vue 实例化时前创建静态路由,用户登录后根据角色信息筛选出动态路由.
  • vue 实例化后通过使用 vue-routeraddRoutes 将动态路由添加到路由表。
  • 根据角色信息过滤路由表生成菜单。

这种方式的优点是不依赖后端,可单独管理,也不需要实现菜单管理这个单独的功能。但也有些缺点:

  • 菜单路由耦合,需要在路由里面配置菜单信息,而且路由不一定是菜单,却多了菜单的配置。
  • 如果需要改菜单的配置比如图标、文案、排序需要前端编码并重新编译部署。

具体代码实现和细节可以看花裤衩大佬的 这篇文章

方案二:前后端配合管理路由菜单

方案一每次都需要前端改动代码修改菜单配置,于是针对这个点可以把菜单路由交给后端管理,比较成熟的代表是 若依后台管理系统 ,它的整体方案和 vue-element-admin 差不多,区别在于菜单和路由是前后端一起管理的。

router/index.js

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

/* Layout */
import Layout from "@/layout";

/**
 * Note: 路由配置项
 *
 * hidden: true                     // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
 * alwaysShow: true                 // 当一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
 *                                  // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
 *                                  // 若想不管路由下面的 children 声明的个数都显示根路由
 *                                  // 可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
 * redirect: noRedirect             // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
 * name:'router-name'               // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
 * roles: ['admin', 'common']       // 访问路由的角色权限
 * permissions: ['a:a:a', 'b:b:b']  // 访问路由的菜单权限
 * meta : {
    noCache: true                   // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
    title: 'title'                  // 设置该路由在侧边栏和面包屑中展示的名字
    icon: 'svg-name'                // 设置该路由的图标,对应路径src/assets/icons/svg
    breadcrumb: false               // 如果设置为false,则不会在breadcrumb面包屑中显示
    activeMenu: '/system/user'      // 当路由设置了该属性,则会高亮相对应的侧边栏。
  }
 */

// 公共路由
export const constantRoutes = [
  {
    path: "/login",
    component: () => import("@/views/login"),
    hidden: true,
  },
  {
    path: "/register",
    component: () => import("@/views/register"),
    hidden: true,
  },
  {
    path: "/404",
    component: () => import("@/views/error/404"),
    hidden: true,
  },
  {
    path: "/401",
    component: () => import("@/views/error/401"),
    hidden: true,
  },
];

// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
  {
    path: "/system/user-auth",
    component: Layout,
    hidden: true,
    permissions: ["system:user:edit"],
    children: [
      {
        path: "role/:userId(\\d+)",
        component: () => import("@/views/system/user/authRole"),
        name: "AuthRole",
        meta: { title: "分配角色", activeMenu: "/system/user" },
      },
    ],
  },
];

// 防止连续点击多次路由报错
let routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
  return routerPush.call(this, location).catch((err) => err);
};

export default new Router({
  mode: "history", // 去掉url中的#
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes,
});

核心拦截器实现和 vue-element-admin 差不多:

permission.js

import router from "./router";
import store from "./store";
import { Message } from "element-ui";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/auth";
import { isRelogin } from "@/utils/request";

NProgress.configure({ showSpinner: false });

const whiteList = ["/login", "/auth-redirect", "/bind", "/register"];

router.beforeEach((to, from, next) => {
  NProgress.start();
  if (getToken()) {
    to.meta.title && store.dispatch("settings/setTitle", to.meta.title);
    /* has token*/
    if (to.path === "/login") {
      next({ path: "/" });
      NProgress.done();
    } else {
      if (store.getters.roles.length === 0) {
        isRelogin.show = true;
        // 判断当前用户是否已拉取完user_info信息
        store
          .dispatch("GetInfo")
          .then(() => {
            isRelogin.show = false;
            store.dispatch("GenerateRoutes").then((accessRoutes) => {
              // 根据roles权限生成可访问的路由表
              router.addRoutes(accessRoutes); // 动态添加可访问路由表
              next({ ...to, replace: true }); // hack方法 确保addRoutes已完成
            });
          })
          .catch((err) => {
            store.dispatch("LogOut").then(() => {
              Message.error(err);
              next({ path: "/" });
            });
          });
      } else {
        next();
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next();
    } else {
      next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页
      NProgress.done();
    }
  }
});

router.afterEach(() => {
  NProgress.done();
});

为了方便理解这个逻辑给画个图:

image.png

用户登录后会请求两个接口数据:

  • 用户信息,包括当前用户角色信息、权限信息,核心结构如下:
{
  "msg": "操作成功",
  "code": 200,
  "permissions": [
    "system:user:resetPwd",
    "system:post:list",
    "monitor:operlog:export",
    "monitor:druid:list"
  ],
  "roles": ["common"],
  "user": {}
}
  • 菜单路由信息,当前用户拥有的路由,核心结构如下:
{
  "msg": "操作成功",
  "code": 200,
  "data": [
    {
      "name": "System",
      "path": "/system",
      "hidden": false,
      "redirect": "noRedirect",
      "component": "Layout",
      "alwaysShow": true,
      "meta": {
        "title": "系统管理",
        "icon": "system",
        "noCache": false,
        "link": null
      },
      "children": [
        {
          "name": "Log",
          "path": "log",
          "hidden": false,
          "redirect": "noRedirect",
          "component": "ParentView",
          "alwaysShow": true,
          "meta": {
            "title": "日志管理",
            "icon": "log",
            "noCache": false,
            "link": null
          },
          "children": [
            {
              "name": "Operlog",
              "path": "operlog",
              "hidden": false,
              "component": "monitor/operlog/index",
              "meta": {
                "title": "操作日志",
                "icon": "form",
                "noCache": false,
                "link": null
              }
            }
          ]
        }
      ]
    }
  ]
}

有了这些数据前端需要再对数据做一层数据,把路由表给弄出来,都知道路由组件在前端一般都是这样的:

{
  name: "login",
  path: "/login",
  component: () => import("@/views/Login.vue")
}

后端是不能直接返回这样的路由的,因为前端代码都是编译后的,已经不认识 @/views/Login.vue 了,所以需要前端进行处理:

export const loadView = (view) => {
  if (process.env.NODE_ENV === "development") {
    return (resolve) => require([`@/views/${view}`], resolve);
  } else {
    // 使用 import 实现生产环境的路由懒加载
    return () => import(`@/views/${view}`);
  }
};

篇幅有限,有兴趣可以直接看若依的源码实现:菜单处理逻辑传送门。下图就是若依的效果截图:

image.png

方案二的优点很明显那就是可以不用部署的方式去修改菜单信息,非常方便,但是也有问题:

  • 菜单路由还是耦合
  • 需要单独开发一个菜单管理功能,前后端的工作量会多出一块
  • 不能随便乱改菜单数据,否则影响整个系统

当然这两个方案可能都不适用,比如还可能存在菜单必须全部展示的情况,不能用的要置灰提示(提示用户充钱才可以用),还有其他什么方案可以在评论区进行讨论的哦!只能说不管是软件开发还是方案选型都没有银弹这一说,适合业务的才是好的!

按钮权限方案

按钮权限一般在前端实现,按钮最终下发的操作是对数据的操作所以本质还是接口权限,后端需要兜底。而前端的场景无非就两种:

  • 无权限时按钮置灰禁用
  • 无权限时按钮直接隐藏

按钮权限稍微麻烦的点在于根据什么去判断权限,角色标识符还是权限点?

这点在# 浅谈 RBAC 权限模型 有提到过基于资源的权限控制,所以基于权限点去做控制比较好,若依也是这样判断的,但是也要补充下根据权限点判断的几种情况:

  • 按钮操作依赖多个权限点时:满足其一或者全部才可以操作,否则就隐藏或者禁用置灰。
  • 按钮操作依赖单个权限点时:满足单个权限点才可以操作,否则就隐藏或者禁用置灰。

基于上述条件可以设计一个权限指令 v-auth

场景一:传入单个权限点:

<button v-auth="'system:user:add'">test</button>

场景二:传入多个权限点:

<!-- 必须满足全部权限点 -->
<button v-auth="['system:user:add', 'system:user:edit']">test</button>

<!-- 满足其中一个权限点 -->
<button v-auth.oneOf="['system:user:add', 'system:user:edit']">test</button>

指令的实现思路就是判断传入的值与全局的权限点进行对比,满足条件后对 DOM 进行操作(移除、置灰...),下面是若依的实现:

import store from "@/store";

export default {
  inserted(el, binding, vnode) {
    const { value } = binding;
    const all_permission = "*:*:*";
    const permissions = store.getters && store.getters.permissions;

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value;

      const hasPermissions = permissions.some((permission) => {
        return (
          all_permission === permission || permissionFlag.includes(permission)
        );
      });

      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error(`请设置操作权限标签值`);
    }
  },
};

要实现 oneOf 的功能只需要对修饰符判断即可,这里就不额外说了,有兴趣可以自己实现。

标签:菜单,浅谈,RBAC,path,import,权限,true,路由
From: https://www.cnblogs.com/wp-leonard/p/17839214.html

相关文章

  • 权限控制基础
    从这个有16.2k星星的后台管理系统项目Vuevbenadmin中看看它是如何做的。获取权限码要做权限控制,肯定需要一个code,无论是权限码还是角色码都可以,一般后端会一次性返回,然后全局存储起来就可以了,Vuevbenadmin是在登录成功以后获取并保存到全局的store中:import{defineStore}......
  • 浅谈基于云计算的环境智能监控系统
    随着经济的飞速发展,环境污染也越来越严重,环境监控成为了政府与社会关注的焦点。本文提出了一种基于云计算的环境智能监控系统——EasyCVR,该系统综合应用了传感器、云计算、大数据、人工智能等技术,具有实时、准确、高效的监控能力,并能够对监控数据进行综合分析和预测,为环保决策提供......
  • 安装 IIS 访问临时文件夹 C:\WINDOWS\TEMP\3C 读取/写入权限 错误: 0x80070005
    在windows中使用命令行方式安装IIS(Web服务器)WindowsServer2022安装IIS报错访问临时文件夹C:\WINDOWS\TEMP\3C读取/写入权限错误:0x80070005,可以使用命令行方式来安装和配置Web服务(IIS)。以下是使用DeploymentImageServicingandManagement(DISM)工具的步骤:1.打......
  • Linux文件权限02
    ACL高级特性最大有效权限mask:使用getfacl,其中mask项就是ACL的最大有效权限注:mask用来指定最大有效权限。系统给用户赋予ACL权限需要和mask的权限逻辑“相与”之后的权限才是用户的真正权限default:继承创建目录dir01,使用setfacl命令给用户增加rwx权限,然后在dir01目录下创建dir0......
  • 浅谈仓储UI自动化之路
    1分层测试分层测试:就是不同的时间段,不同的团队或团队使用不同的测试用例对产品不同的关注点进行测试。一个系统/产品我们最先看到的是UI层,也就是外观或者说整体,这些是最上层,最上层依赖下面的服务层,也就是接口或者模块,最底层就是单元,这个单元是函数或者方法。按照这三层选择不同......
  • 文件权限
    一、基本权限UGO1.基本介绍(1)U:owner,属组用户(User):这是文件或目录的所有者,拥有最高的控制权。用户可以读取、写入和执行文件,也可以修改它们的权限。例如,如果一个文件的属主是Alice,那么Alice可以读取、写入和执行这个文件。     G:group,属组组(Group):组权限适用于文......
  • 文件权限
    引言在Linux系统中,文件权限是保护文件安全性的重要机制之一。正确地设置文件权限可以确保只有授权用户能够访问文件,从而保障系统的安全性。本文将深入探讨Linux中文件权限的各个方面,包括基本权限UGO、基本权限ACL、高级权限、文件属性chattr以及进程掩码umask。1.基本权限UGOL......
  • Linux文件权限管理详解
    Linux文件权限表示方式
在Linux系统中,文件权限使用数字表示法,每组权限用三位二进制数表示,分别为文件所有者的读、写和执行权限;和所有者同组的用户的读、写和执行权限;系统中其他用户的读、写和执行权限。
例如,一个文件的权限为755,表示文件所有者具有读、写、执行权限(7),同组用户具......
  • oracle创建用户授权提示无权限解决方案
    流程如下:1.win+r输入cmd回车,打开命令行窗口,输入 sqlplus 用户名/密码assysdba以管理员身份连接数据库。    如:sqlplusscott/123456assysdba;2.创建用户并授权--创建用户createuser用户名identifiedby密码;--授予所有权限grantallprivilegesto......
  • 深度解剖Linux权限的概念
    Linux权限系统是其安全性的基石,它允许系统管理员和用户对文件和目录进行精细的控制。在深度解剖Linux权限的概念时,我们需要涵盖以下主题:1.**文件系统基础**  -文件系统结构:Linux文件系统以树状结构组织,包括根目录、子目录和文件。  -文件属性:每个文件都有一组属......