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

13次阅读

共计 7193 个字符,预计需要花费 18 分钟才能阅读完成。

前两篇关于 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/abott@adorable.png”
}
}
设置菜单
将固定菜单 (/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>
参数可为 function、role,表明以功能编码或角色编码进行校验,为空则使用两者进行校验。
修饰符 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”])
};
},
默认同时使用角色编码与功能编码进行校验,有一项匹配即可。
类似的方法还要 hasFunctions,hasRoles。
源码位置在 plugin/permission/index.js,根据自己实际需求进行修改。
不要使用 v -if=”hasPermissions([‘p_menu_edit’])” 这种方式,会导致方法多次执行
也可以直接在组件中从 vuex store 读取权限信息进行校验。
开发建议

页面级别的组件放到 pages/ 目录下,并且在 routerMapCompnonents/index.js 中以 key-value 的形式导出
不需要权限控制的固定菜单放到 menu/aside.js 和 menu/header.js 中
不需要权限控制的路由放到 router/routes.js frameIn 内
需要权限控制的菜单与路由通过界面的管理功能进行添加,确保菜单的 path 与路由的 path 相对应,路由的 name 与页面组件的 name 一致才能使 keep-alive 生效,路由的 component 在 routerMapCompnonents/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 文档
前端代码生成还在开发中 …

正文完
 0