vue基于d2-admin的RBAC权限管理解决方案

前两篇关于vue权限路由文章的填坑,说了一堆理论,是时候操作一波了。

vue权限路由实现方式总结

vue权限路由实现方式总结二

选择d2-admin是因为element-ui的相关开源项目里,d2-admin的结构和代码是让我感到最舒服的,而且基于d2-admin实现RBAC权限管理也很方便,对d2-admin没有大的侵入性的改动。

预览地址

Github

相关概念

不了解RBAC,可以看这里企业管理系统前后端分离架构设计 系列一 权限模型篇

  • 实现了RBAC模型权限控制
  • 菜单与路由独立管理,完全由后端返回
  • user存储用户
  • admin标识用户是否为系统管理员
  • role存储角色信息
  • roleUser存储用户与角色的关联关系
  • menu存储菜单信息,类型分为菜单功能,一个菜单下可以有多个功能,菜单类型的permission字段标识访问这个菜单需要的功能权限,功能类型的permission字段相当于此功能的别称,所以菜单类型的permission字段为其某个功能类型子节点的permission
  • permission存储角色与功能的关联关系
  • interface存储接口信息
  • functionInterface存储功能与接口关联关系,通过查找用户所属角色,再查找相关角色所具备的功能权限,再通过相关功能就可以查出用户所能访问的接口
  • route存储前端路由信息,通过permission字段过滤出用户所能访问的路由

运行流程及相关API

使用d2admin的原有登录逻辑,全局路由守卫中判断是否已经拉取权限信息,获取后标识为已获取。

const token = util.cookies.get('token')
    if (token && token !== 'undefined') {
      //拉取权限信息
      if (!isFetchPermissionInfo) {
        await fetchPermissionInfo();
        isFetchPermissionInfo = true;
        next(to.path, true)
      } else {
        next()
      }
    } else {
      // 将当前预计打开的页面完整地址临时存储 登录后继续跳转
      // 这个 cookie(redirect) 会在登录后自动删除
      util.cookies.set('redirect', to.fullPath)
      // 没有登录的时候跳转到登录界面
      next({
        name: 'login'
      })
    }
//标记是否已经拉取权限信息
let isFetchPermissionInfo = false

let fetchPermissionInfo = async () => {
  //处理动态添加的路由
  const formatRoutes = function (routes) {
    routes.forEach(route => {
      route.component = routerMapComponents[route.component]
      if (route.children) {
        formatRoutes(route.children)
      }
    })
  }
  try {
    let userPermissionInfo = await userService.getUserPermissionInfo()
    permissionMenu = userPermissionInfo.accessMenus
    permissionRouter = userPermissionInfo.accessRoutes
    permission.functions = userPermissionInfo.userPermissions
    permission.roles = userPermissionInfo.userRoles
    permission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces)
    permission.isAdmin = userPermissionInfo.isAdmin == 1
  } catch (ex) {
    console.log(ex)
  }
  formatRoutes(permissionRouter)
  let allMenuAside = [...menuAside, ...permissionMenu]
  let allMenuHeader = [...menuHeader, ...permissionMenu]
  //动态添加路由
  router.addRoutes(permissionRouter);
  // 处理路由 得到每一级的路由设置
  store.commit('d2admin/page/init', [...frameInRoutes, ...permissionRouter])
  // 设置顶栏菜单
  store.commit('d2admin/menu/headerSet', allMenuHeader)
  // 设置侧边栏菜单
  store.commit('d2admin/menu/fullAsideSet', allMenuAside)
  // 初始化菜单搜索功能
  store.commit('d2admin/search/init', allMenuHeader)
  // 设置权限信息
  store.commit('d2admin/permission/set', permission)
  // 加载上次退出时的多页列表
  store.dispatch('d2admin/page/openedLoad')
  await Promise.resolve()
}

后端需要返回的权限信息包括权限过滤后的角色编码集合,功能编码集合,接口信息集合,菜单列表,路由列表,以及是否系统管理员标识。格式如下

{
  "statusCode": 200,
  "msg": "",
  "data": {
    "userName": "MenuManager",
    "userRoles": [
      "R_MENUADMIN"
    ],
    "userPermissions": [
      "p_menu_view",
      "p_menu_edit",
      "p_menu_menu"
    ],
    "accessMenus": [
      {
        "title": "系统",
        "path": "/system",
        "icon": "cogs",
        "children": [
          {
            "title": "系统设置",
            "icon": "cogs",
            "children": [
              {
                "title": "菜单管理",
                "path": "/system/menu",
                "icon": "th-list"
              }
            ]
          },
          {
            "title": "组织架构",
            "icon": "pie-chart",
            "children": [
              {
                "title": "部门管理",
                "icon": "html5"
              },
              {
                "title": "职位管理",
                "icon": "opencart"
              }
            ]
          }
        ]
      }
    ],
    "accessRoutes": [
      {
        "name": "System",
        "path": "/system",
        "component": "layoutHeaderAside",
        "componentPath": "layout/header-aside/layout",
        "meta": {
          "title": "系统设置",
          "cache": true
        },
        "children": [
          {
            "name": "MenuPage",
            "path": "/system/menu",
            "component": "menu",
            "componentPath": "pages/sys/menu/index",
            "meta": {
              "title": "菜单管理",
              "cache": true
            }
          },
          {
            "name": "RoutePage",
            "path": "/system/route",
            "component": "route",
            "componentPath": "pages/sys/route/index",
            "meta": {
              "title": "路由管理",
              "cache": true
            }
          },
          {
            "name": "RolePage",
            "path": "/system/role",
            "component": "role",
            "componentPath": "pages/sys/role/index",
            "meta": {
              "title": "角色管理",
              "cache": true
            }
          },
          {
            "name": "UserPage",
            "path": "/system/user",
            "component": "user",
            "componentPath": "pages/sys/user/index",
            "meta": {
              "title": "用户管理",
              "cache": true
            }
          },
          {
            "name": "InterfacePage",
            "path": "/system/interface",
            "component": "interface",
            "meta": {
              "title": "接口管理"
            }
          }
        ]
      }
    ],
    "accessInterfaces": [
      {
        "path": "/menu/:id",
        "method": "get"
      },
      {
        "path": "/menu",
        "method": "get"
      },
      {
        "path": "/menu/save",
        "method": "post"
      },
      {
        "path": "/interface/paged",
        "method": "get"
      }
    ],
    "isAdmin": 0,
    "avatarUrl": "https://api.adorable.io/avatars/85/[email protected]"
  }
}

设置菜单

将固定菜单(/menu/header/menu/aside)与后端返回的权限菜单(accessMenus)合并后,存入相应的vuex store模块中

...
let allMenuAside = [...menuAside, ...permissionMenu]
let allMenuHeader = [...menuHeader, ...permissionMenu]
...
// 设置顶栏菜单
store.commit('d2admin/menu/headerSet', allMenuHeader)
// 设置侧边栏菜单
store.commit('d2admin/menu/fullAsideSet', allMenuAside)
// 初始化菜单搜索功能
store.commit('d2admin/search/init', allMenuHeader)

处理路由

默认使用routerMapComponents 的方式处理后端返回的权限路由

//处理动态添加的路由
const formatRoutes = function (routes) {
    routes.forEach(route => {
        route.component = routerMapComponents[route.component]
        if (route.children) {
        formatRoutes(route.children)
        }
    })
}
...
formatRoutes(permissionRouter)
//动态添加路由
router.addRoutes(permissionRouter);
// 处理路由 得到每一级的路由设置
store.commit('d2admin/page/init', [...frameInRoutes, ...permissionRouter])

路由处理方式及区别可看vue权限路由实现方式总结二

设置权限信息

将角色编码集合,功能编码集合,接口信息集合,以及是否系统管理员标识存入相应的vuex store模块中

...
permission.functions = userPermissionInfo.userPermissions
permission.roles = userPermissionInfo.userRoles
permission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces)
permission.isAdmin = userPermissionInfo.isAdmin == 1
...
// 设置权限信息
store.commit('d2admin/permission/set', permission)

接口权限控制以及loading配置

支持使用角色编码,功能编码以及接口权限进行控制,如下

export function getMenuList() {
    return request({
        url: '/menu',
        method: 'get',
        interfaceCheck: true,
        permission:["p_menu_view"],
        loading: {
            type: 'loading',
            options: {
                fullscreen: true,
                lock: true,
                text: '加载中...',
                spinner: 'el-icon-loading',
                background: 'rgba(0, 0, 0, 0.8)'
            }
        },
        success: {
            type: 'message',
            options: {
                message: '加载菜单成功',
                type: 'success'
            }
        }
    })
}

interfaceCheck: true表示使用接口权限进行控制,如果vuex store中存储的接口信息与当前要请求的接口想匹配,则可发起请求,否则请求将被拦截。

permission:["p_menu_view"]表示使用角色编码和功能编码进行权限校验,如果vuex store中存储的角色编码或功能编码与当前表示的编码相匹配,则可发起请求,否则请求将被拦截。

源码位置在libs/permission.js,可根据自己需求进行修改

loading配置相关源码在libs/loading.js,根据自己需求进行配置,success也是如此,源码在libs/loading.js。 照此思路可以自行配置其它功能,比如请求失败等。

页面元素权限控制

使用指令v-permission

 <el-button
    v-permission:function.all="['p_menu_edit']"
    type="primary"
    icon="el-icon-edit"
    size="mini"
    @click="batchEdit"
    >批量编辑</el-button>

参数可为functionrole,表明以功能编码或角色编码进行校验,为空则使用两者进行校验。

修饰符all,表示必须全部匹配指令值中所有的编码。

源码位置在plugin/permission/index.js,根据自己实际需求进行修改。

使用v-if+全局方法:

<el-button
    v-if="canAdd"
    type="primary"
    icon="el-icon-circle-plus-outline"
    size="mini"
    @click="add"
    >添加</el-button>
data() {
    return {
      canAdd: this.hasPermissions(["p_menu_edit"])
    };
  },

默认同时使用角色编码与功能编码进行校验,有一项匹配即可。

类似的方法还要hasFunctionshasRoles

源码位置在plugin/permission/index.js,根据自己实际需求进行修改。

不要使用v-if="hasPermissions([‘p_menu_edit‘])"这种方式,会导致方法多次执行

也可以直接在组件中从vuex store读取权限信息进行校验。

开发建议

  • 页面级别的组件放到pages/目录下,并且在routerMapCompnonents/index.js中以key-value的形式导出
  • 不需要权限控制的固定菜单放到menu/aside.jsmenu/header.js
  • 不需要权限控制的路由放到router/routes.js frameIn
  • 需要权限控制的菜单与路由通过界面的管理功能进行添加,确保菜单的path与路由的path相对应,路由的name与页面组件的name一致才能使keep-alive生效,路由的componentrouterMapCompnonents/index.js中能通过key匹配到。
  • 开发阶段菜单与路由的添加可由开发人员自行维护,并维护一份清单,上线后将清单交给相关的人去维护即可。

如果觉得麻烦,不想菜单与路由由后端返回,可以在前端维护一份菜单和路由(路由中的component还是使用字符串,参考mock/permissionMenuAndRouter.js),并且在菜单和路由上面维护相应的权限编码,一般都是使用功能编码。后端就不需要返回菜单和路由信息了,但是其他权限信息,比如角色编码,功能编码等还是需要的。通过后端返回的功能编码列表,在前端过滤出用户具备权限的菜单和路由,过滤处理后后的菜单与路由格式与之前由后端返回的格式一致,然后将处理后的菜单与路由当做后端返回的一样处理即可。

数据mock与代码生成

数据mock使用lazy-mock修改而来的d2-admin-server,数据真实来源于后端,相比其他工具,支持数据持久化,存储使用的是json文件,不需要安装数据库。简单的配置即可自动生成增删改查的接口。

后端使用中间件控制访问权限,比如:

 .get('/menu', PermissionCheck(), controllers.menu.getMenuList)

PermissionCheck默认使用接口进行校验,校验用户所能访问的API中是否匹配当前API,支持使用功能编码与角色编码进行校验PermissionCheck(["p_menu_edit"],["r_menu_admin"],true),第一个参数为功能编码,第二个为角色编码,第三个为是否使用接口进行校验。

更多详细用法可看lazy-mock文档

前端代码生成还在开发中...

原文地址:https://www.cnblogs.com/jaycewu/p/10228936.html

时间: 2024-10-07 10:56:45

vue基于d2-admin的RBAC权限管理解决方案的相关文章

基于RBAC权限管理模型学习

在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限.这就极大地简化了权限的管理. 在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色.角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收. 角色与角色的关系可以建立起来以囊括更广泛的客观情况. BAC支持三个著名的安全原则:最小权限原则,责任分离原则和数据抽象原则. (1)最小权限原则之所以被RBAC所支持,是因

RBAC权限管理

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限. 这样,就构造成“用户-角色-权限”的授权模型.在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系.(如下图) 角色是什么?可以理解为一定数量的权限的集合,权限的载体.例如:一个论坛系统,“超级管理员”.“版主”都是角色.版主可管理版内的帖子.可管理版内的用户等,这些是权限.要给某个用户授予这些权限,不需要直接

使用SpringSecurity3实现RBAC权限管理

1. What? 什么是权限管理? 具体可参见百度:http://baike.baidu.com/view/2108713.htm 名词备注: 数据级权限:百科内的权限管理一文解释的比较不错,但其中的"数据级权限"有的人看来会觉得有点摸不着头脑.数据级权限,即表示权限与特定数据有联系的权限,比方说,某用户只能创建100个用户.这个100,就是数据级权限的一个指标. 2. How?怎么样实现权限管理? 2.1.一种烦恼 也许很多程序员会在权限管理中遇到这样的一个问题. 大部分项目都需要权

go web 权限管理 解决方案 (面向对象权限 ABAC / Casbin)

go web 权限管理 解决方案 (面向对象权限 ABAC / Casbin) 说明 ABAC调用 github.com/casbin/casbin abac_model.conf [request_definition] r = sub, obj, act [policy_definition] p = sub, obj,act [policy_effect] e = some(where (p.eft == allow)) [matchers] m = r.sub.App == r.obj.

基于Django实现RBAC权限管理

概述 RBAC(Role-Based Access Control,基于角色的访问控制),通过角色绑定权限,然后给用户划分角色.在web应用中,可以将权限理解为url,一个权限对应一个url. 在实际应用中,url是依附在菜单下的,比如一个简单的生产企业管理系统,菜单可以大致分为以下几块:制造.资材.生产管理.人事.财务等等.每个菜单下又可以有子菜单,但最终都会指向一个url,点击这个url,通过Django路由系统执行一个视图函数,来完成某种操作.这里,制造部的员工登录系统后,肯定不能点击财务

基于RBAC权限管理

一般web系统操作人员多时都会需求权限管理,一来限制操作范围,二来限制数据公开度. 现在最流行的一个模式为 RBAC (Role-Based Access Control) 基于角色的访问控制.设定权限范围定义到角色中,然后再分配到每个用户. 这里仅以一般后台管理系统为例,叙说数据结构: 需求: 菜单需要针对不同部门使用不同的菜单结构. 权限项能精确到页面中某个内容或局部功能. 基本要求:没有权限的菜单,页面中内容或链接禁止显示. 表结构 CREATE TABLE `power_item` (

ThinkPHP RBAC权限管理机制

RBAC是ThinkPHP很好用的后台权限管理的,话不多说,实现方法如下,也方便以后自己查询使用: 1.新建4个数据库表 self_role权限表 CREATE TABLE `self_role` ( `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL, `pid` smallint(6) DEFAULT NULL, `status` tinyint(1) unsigned DEFAULT

【转】RBAC权限管理

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成“用户-角色-权限”的授权模型.在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系.(如下图) 角色是什么?可以理解为一定数量的权限的集合,权限的载体.例如:一个论坛系统,“超级管理员”.“版主”都是角色.版主可管理版内的帖子.可管理版内的用户等,这些是权限.要给某个用户授予这些权限,不需要直接将

Yii2-admin RBAC权限管理的实现

yii2-admin是yii2 rbac的一套管理工具,实现了漂亮的界面和完整的权限管理功能,不用自己再去写权限代码了,使用之前请将yii2的源码更新到最新版本. git源码地址:https://github.com/mdmsoft/yii2-admin 安装yii2-admin: 1.首先切换到项目目录下 2.执行该语句:composer.phar require mdmsoft/yii2-admin 注:如果提示could not open input file composer.phar