【低代码】角色和权限的解决方案
低代码的角色和权限
基于Vue3 + UI 库,实现角色和权限的维护,依赖低代码的 JSON 而设计。
普通项目也可以使用,只是由于没有现成的 JSON,显得稍微麻烦一些。
角色和权限,我是一直都觉得挺简单的,甚至都不需要一开始就去设计角色。因为,开发项目的时候,按照“原子”级别设置功能模块,然后把权限设置到字段。这样,项目开发完毕再去设置角色也不迟,甚至可以让客户自行维护
相关文章
- 专栏 基于Vue3做一套低代码引擎
- 开篇
- 列表
- 表单
- 角色和权限
- 查询(编写中)
- 操作按钮(编写中)
主要内容
- 权限的分类
- 权限、角色、用户的关系
- 定义相关的接口
- 实现角色的权限的维护方式
权限的分类
以前权限可以分为操作权限和资源权限,现在前后端分离,可以加上后端API的权限。
- 操作权限:用户可以做哪些操作,比如打开某个模块,使用添加、修改、审批等操作。
- 资源权限:用户可以使用哪些数据,比如只能维护自己的客户信息,不能看别人的客户信息等。
- 后端API权限:用户可以使用哪些API。
整理一下画了一个脑图:
其中后端 API 可以和操作按钮相对应,比如拥有【添加按钮】的权限的话,那么就应该有对应的后端 API 的权限,否则如何提交数据呢?
当然还有一些后端 API 和操作按钮没有明显的对应关系,这种情况需要单独设置,比如访问“字典”的权限。
权限、角色和用户的关系
一个角色可以有“一组”权限,一个用户可以有多个角色,一个角色也可以有多个用户。 这样就建立了一个简单的关联关系。
定义接口
总体思路有了我们开始设计接口。 好吧,其实以前是直接设计关系型数据库的,不过现在喜欢先设计接口。
- 角色和权限
- 角色里的用户
- 权限的备选容器
- 模块列表
- 模块里的操作按钮
- 模块的列
- 模块的查询条件
- 模块的资源权限
- 模块的后端API
IRole 角色和权限
一个角色拥有哪些权限?我们来罗列一下:
type idType = number | string
/**
* (一个)角色拥有的一组权限
*/
export interface IRole {
roleId: idType, // 角色编号
roleName: string, // 角色名称
rolePower: { // 角色拥有的权限集合
moduleIds: Array<idType>, // 可以使用哪些模块
buttonIds: {
[moduleId: idType]: Array<idType> // 模块里面可以使用的操作按钮
},
gridIds: {
[moduleId: idType]: Array<idType> // 模块里可以使用的列(字段)
},
findIds: {
[moduleId: idType]: Array<idType> // 模块里可以使用的查询条件(字段)
},
gridIdsNot: {
[moduleId: idType]: Array<idType> // 列表里面不可以用的列
},
findIdsNot: {
[moduleId: idType]: Array<idType> // 列表里面不可以使用的查询字段
},
resources: {
[moduleId: idType]: Array<idType> // 模块可以加载的资源权限
},
APIs: {
[moduleId: idType]: Array<idType> // 模块里的特殊的后端API
}
}
}
IRoleUser 角色的用户
/**
* 角色和用户的关联,基于关系型数据库
*/
export interface IRoleUser {
roleUserId: idType,
roleId: idType,
userId: idType,
}
/**
* 一个用户可以用多个角色
*/
export interface IUserRole {
userId: idType,
roleIds: Array<idType>
}
IRoleUser 是按照关系型数据库设置的,IUserRole 是按照对象的思路做的,一个用户有哪些角色。
IRoleData 权限的备选项容器
维护权限,需要先准备好基础信息,比如模块信息、操作按钮信息等,我们来设计一个接口:
/**
* 维护角色的准备数据
*/
export interface IRoleData {
modules: IRoleModule[], // 模块信息,绑定 el-tree
buttons: { // 模块里面的操作按钮,绑定 模块里的按钮
[moduleId: idType]: Array<IRoleButton>
},
grids: { // 模块里面的列
[moduleId: idType]: Array<IRoleColumn>
},
finds: { // 模块里面的查询条件
[moduleId: idType]: Array<IRoleColumn>
},
resources: { // 模块里面可以选择的资源权限
[moduleId: idType]: Array<IRoleColumn>
},
APIs: { // 模块里面可以选择的(其他)后端API
[moduleId: idType]: Array<IRoleColumn>
}
}
用于建立设置权限的界面,比如有哪些模块,模块里的按钮、列等备选信息。
IRoleModule 模块信息
主要用于绑定 el-tree,n 级分组。
/**
* 记录 功能模块 信息,用于绑定 el-tree
*/
export interface IRoleModule {
id: idType, // 模块ID
label: string,
children?: IRoleModule[]
}
IRoleButton 模块的操作按钮
一个模块里面有哪些操作按钮?需要记录一下。总不能固定为添加、修改、删除吧。
/**
* 记录 操作按钮 信息
*/
export interface IRoleButton {
buttonId: idType, // value
moduleId: idType,
label: string, // label
kind: string | 'add' | 'update' | 'delete' | 'look' | 'detail' | 'list', // 按钮类型,增删改查等
}
IRoleColumn 权限到字段
细粒度的权限,需要可以到“字段”这个级别,比如权限到列表的字段,权限到查询的字段,权限到表单的字段等。
另外,由于资源权限和后端API,需要的结构和 IRoleColumn 其实是一样的,所以就不单独设置接口了。
/**
* 记录 模块的列表、查询字段、资源权限、后端API。
* * 一个模块只有一个列表。
* * 一个模块只有一组查询字段。
* * 模块可以选择的资源权限
* * 模块可以选择的后端API
*/
export interface IRoleColumn {
value: idType, // 字段ID
label: string, // 字段名称/API 名称/资源权限名称
}
资源权限和后端API
资源权限也可以做个目录,然后和模块关联,以备选择。
/**
* 模块可以选择的资源权限,或者后端API
*/
export interface IRoleResourcesOrAPI {
id: idType, // 资源权限的编号
label: string, // 资源权限的名称
}
接口定义好了,然后我们看看编码的实现方式。
实现角色和权限的维护
角色的一组权限是一个整体,应该一同显示出来,所以需要我们先把各种“可选项”罗列出来,然后设置已经有的权限,便于让用户调整角色的权限。
定义状态
因为功能分散在多个组件里面实现,为了更好的共享数据,这里没有采用 props 的传递方式,而是采用了“局部状态”的方式,所以我们先定义一套状态:
import type { InjectionKey } from 'vue'
import { watch } from 'vue'
import { defineStore, useStoreLocal } from '@naturefw/nf-state'
import type { IState } from '@naturefw/nf-state/dist/type'
import type {
IRoleData
} from '../types/10-role'
const flag = Symbol('role') as InjectionKey<string>
/**
* 注册局部状态
* * 角色管理用的状态 : IRoleData & IState <IRoleData>
* @returns
*/
export const regRoleState = (): IRoleData & IState => {
// 定义 角色用的状态
const state = defineStore<IRoleData>(flag, {
state: (): IRoleData => {
return {
modules: [], // 模块信息,绑定 tree
buttons: {}, // 模块里面的操作按钮,绑定 模块里的按钮
grids: {}, // 模块里面的列
finds: {}, // 模块里面的查询条件
resources: {}, // 模块里面可以选择的资源权限
APIs: {}, // 模块里面可以选择的(其他)后端API
haveCols: {}, // 模块是否有列、资源权限等选项
roleInfo: { // 当前的角色的信息
roleId: 0,
roleName: '默认' ,
rolePower: { // 角色拥有的权限集合
moduleIds: [], // 权限到【模块】
buttonIds: {}, // 权限到【按钮】,按钮ID集合
gridIds: {}, // 权限到【列表】字段,列表里的字段ID集合
findIds: {}, // 权限到【查询】字段,查询里的字段ID集合
gridIdsNot: {}, // 【列表】里的字段ID集合,不允许使用的
findIdsNot: {}, // 【查询】里的字段ID集合,不允许使用的
resources: {}, // 可以使用的【资源权限】
APIs: {} // 可以使用的【API】
}
}
}
},
actions: {
/**
* 加载数据,
*/
async loadData () {
// 加载数据
}
}
},
{ isLocal: true }
)
return state
}
/**
* 子组件获取状态
*/
export const getRoleState = (): IRoleData & IState => {
return useStoreLocal<IRoleData & IState>(flag)
}
制作菜单
可以使用 el-tree 实现功能菜单的展示,自带一些关联选择等功能,还是比较方便的。
<el-tree
ref="treeRef"
:data="state.modules"
:node-key="nodeKey"
:props="defaultProps"
:current-node-key="roleInfo.currentNodeKey"
show-checkbox
:expand-on-click-node="false"
:check-on-click-node="true"
default-expand-all
highlight-current
empty-text="加载中"
:default-expanded-keys="[1]"
@node-click="handleNodeClick"
@check-change="checkChange"
>
<template #default="{ node, data }">
<span class="custom-tree-node" @click="mychange($event)">
<span>
{{ node.label }}
</span>
<span>
<!--模块的操作按钮-->
<role-button :node="node" :moduleId="data[nodeKey]"></role-button>
<!--模块的操作按钮-->
<span v-show="state.haveCols[data[nodeKey]]" >
<el-popover
placement="left"
:width="400"
sytle="height:400px;"
trigger="click"
>
<template #reference>
<el-button style="margin-right: 16px" size="small">字段</el-button>
</template>
<!--模块的列表--> <br><br>
<role-grid kind="gridIds" placeholder="权限到列表的列" :moduleId="data[nodeKey]"></role-grid>
<!--模块的列表--> <br><br>
<role-grid kind="gridIdsNot" placeholder="不可以使用的列" :moduleId="data[nodeKey]"></role-grid>
<!--模块的查询--> <br><br>
<role-grid kind="findIds" placeholder="权限到查询" :moduleId="data[nodeKey]"></role-grid>
<!--模块的查询--> <br><br>
<role-grid kind="findIdsNot" placeholder="不可用的查询字段" :moduleId="data[nodeKey]"></role-grid>
<!--模块的资源权限--> <br><br>
<role-grid kind="resources" placeholder="资源权限" :moduleId="data[nodeKey]"></role-grid>
<!--模块的后端API--> <br><br>
<role-grid kind="APIs" placeholder="可用的后端API" :moduleId="data[nodeKey]"></role-grid>
</el-popover>
</span>
<span v-show="!state.haveCols[data[nodeKey]]"> </span>
</span>
</span>
</template>
</el-tree>
模块的操作按钮
为了拆分代码(便于维护),我们做一个组件实现权限到操作按钮的功能,然后把这个组件放入 el-tree 的slot 里面。
<el-checkbox
style="width:85px"
v-for="(item, index) in list"
:key="'check' + index + '_' + item.buttonId"
v-model="roleButton[item.buttonId]"
@click="mychange($event, item.buttonId)"
>
{{item.label}}
</el-checkbox>
这里还遇到一个小问题,如果使用 el-checkbox-group 的话,会报错,所以只好使用 el-checkbox 了。
import { getRoleState } from '../state/state-role'
const state = getRoleState()
const list = state.buttons[props.moduleId]
const roleButton = reactive({})
// 如果有按钮,设置选项值
if (list) {
// 设置 check 的选项值
list.forEach((item) => {
roleButton[item.buttonId] = false
})
}
const mychange = (e, buttonId) => {
// 防止事件冒泡
e.stopPropagation()
}
这里需要使用 e.stopPropagation() 阻止事件冒泡。
权限到字段
思路和操作按钮一致,只是这里可以设定几个安全级别:
- 宽松:不设置可以访问的列,表示可以使用模块里所有的列 —— 便于设置角色。
- 严谨:必须设置可以使用的列,没有设置的话不可以访问 —— 提高安全性。
- 预防:敏感列放入“不可访问”名单,可以在一定程度上避免误操作 —— 折中方案。
宽松级别,便于设置角色的权限,因为大部分情况下,可以使用这个模块的话,那么就意味着模块里的列都是可以访问的,如果必须设置列,那么有点繁琐,还容易点错。
严谨级别是对于要求严格的项目而设置,为了更安全,必须设置可以访问的列。虽然更安全,但是显然做设置的时候比较繁琐。
预防级别,这是一个折中的方案,既然大部分字段都可以访问,只有个别的不能访问,那么我们把这几个敏感字段标注起来,列入“黑名单”。
<span v-if="list.length > 0">
{{title[kind]}}
</span>
<el-select-v2
v-if="list.length > 0"
v-model="value"
:options="list"
size="small"
:placeholder="placeholder"
style="width: 240px;"
multiple
clearable
collapse-tags
collapse-tags-tooltip
:height="300"
@click="mychange($event)"
:teleported="false"
/>
一开始用的是 el-select ,发现报错了。还好 el-select-v2 没有报错,否则就麻烦了。
资源权限
资源权限,主要是后端的事情,因为前端不应该拿到没有权限的数据。
简单的说呢,非常简单,我们首先看一下SQL
select * from table1 where 【userId = xxx 】 and xxx = xxxx ...
当然有个前提,项目使用关系型数据库。
资源权限,最根本的就是上面 【】内部的部分,不管中间过程如何,最后都会归结为如何写SQL(where 后面的查询条件)
好像有点跑题,维护的时候,只需要根据情况做选择即可,表现形式和权限到字段是一样的,所以就用了同一个组件,然后内部做一下判断,区分选项来源即可。
const dic = {
gridIds: 'grids',
gridIdsNot: 'grids',
findIds: 'finds',
findIdsNot: 'finds',
resources: 'resources',
APIs: 'APIs'
}
const value = ref([])
const { kind } = props
// 获取状态
const state = getRoleState()
// 根据类型,获取下拉列表的备选项
const list = state[dic[kind]][props.moduleId]?? reactive([])
if (state.haveCols[props.moduleId]) {
if (state.haveCols[props.moduleId] === false) {
state.haveCols[props.moduleId] = (list.length > 0)
}
} else {
state.haveCols[props.moduleId] = (list.length > 0)
}
// 监听选项值,设置角色
watch(value, () => {
if (value.value.length === 0) {
delete state.roleInfo.rolePower[kind][props.moduleId]
} else {
state.roleInfo.rolePower[kind][props.moduleId] = value.value
}
})
根据用户的选择,设置角色可以拥有的权限。
小结
角色的权限的设置方面基本就是这样了。用Vue3 + UI 库实现功能,方便了很多。以前用jQuery,一些功能需要自己实现,现在UI库搞定了各种基础操作,我们整合一下即可。
权限设置完毕,下面就是在项目里面如何使用的问题了。 低代码的话比较容易,因为低代码是依赖JSON渲染的,而权限,其实说白了,就是规定可以加载哪些JSON。
转载自:https://juejin.cn/post/7121479241102786597