【低代码】角色和权限的解决方案

·  阅读 4493
【低代码】角色和权限的解决方案

低代码的角色和权限

基于Vue3 + UI 库,实现角色和权限的维护,依赖低代码的 JSON 而设计。

普通项目也可以使用,只是由于没有现成的 JSON,显得稍微麻烦一些。

角色和权限,我是一直都觉得挺简单的,甚至都不需要一开始就去设计角色。因为,开发项目的时候,按照“原子”级别设置功能模块,然后把权限设置到字段。这样,项目开发完毕再去设置角色也不迟,甚至可以让客户自行维护

相关文章

主要内容

  • 权限的分类
  • 权限、角色、用户的关系
  • 定义相关的接口
  • 实现角色的权限的维护方式

权限的分类

以前权限可以分为操作权限和资源权限,现在前后端分离,可以加上后端API的权限。

  • 操作权限:用户可以做哪些操作,比如打开某个模块,使用添加、修改、审批等操作。
  • 资源权限:用户可以使用哪些数据,比如只能维护自己的客户信息,不能看别人的客户信息等。
  • 后端API权限:用户可以使用哪些API。

整理一下画了一个脑图:

权 限.png

其中后端 API 可以和操作按钮相对应,比如拥有【添加按钮】的权限的话,那么就应该有对应的后端 API 的权限,否则如何提交数据呢?

当然还有一些后端 API 和操作按钮没有明显的对应关系,这种情况需要单独设置,比如访问“字典”的权限。

权限、角色和用户的关系

一个角色可以有“一组”权限,一个用户可以有多个角色,一个角色也可以有多个用户。
这样就建立了一个简单的关联关系。

权 限2.png

定义接口

总体思路有了我们开始设计接口。
好吧,其实以前是直接设计关系型数据库的,不过现在喜欢先设计接口。

  • 角色和权限
  • 角色里的用户
  • 权限的备选容器
    • 模块列表
    • 模块里的操作按钮
    • 模块的列
    • 模块的查询条件
    • 模块的资源权限
    • 模块的后端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, // 资源权限的名称
}
复制代码

接口定义好了,然后我们看看编码的实现方式。

实现角色和权限的维护

角色的一组权限是一个整体,应该一同显示出来,所以需要我们先把各种“可选项”罗列出来,然后设置已经有的权限,便于让用户调整角色的权限。

100权限到模块和按钮.png

定义状态

因为功能分散在多个组件里面实现,为了更好的共享数据,这里没有采用 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>
          <!--模块的操作按钮--> &nbsp;
          <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>
              <!--模块的列表--> &nbsp; <br><br>
              <role-grid  kind="gridIds" placeholder="权限到列表的列" :moduleId="data[nodeKey]"></role-grid>
              <!--模块的列表--> &nbsp; <br><br>
              <role-grid  kind="gridIdsNot" placeholder="不可以使用的列" :moduleId="data[nodeKey]"></role-grid>
              <!--模块的查询--> &nbsp; <br><br>
              <role-grid  kind="findIds" placeholder="权限到查询" :moduleId="data[nodeKey]"></role-grid>
              <!--模块的查询--> &nbsp; <br><br>
              <role-grid  kind="findIdsNot" placeholder="不可用的查询字段" :moduleId="data[nodeKey]"></role-grid>
              <!--模块的资源权限--> &nbsp; <br><br> 
              <role-grid  kind="resources" placeholder="资源权限" :moduleId="data[nodeKey]"></role-grid>
              <!--模块的后端API--> &nbsp; <br><br>
              <role-grid  kind="APIs" placeholder="可用的后端API" :moduleId="data[nodeKey]"></role-grid>
            </el-popover>
          </span>
          <span v-show="!state.haveCols[data[nodeKey]]"> &nbsp; &nbsp;  &nbsp; &nbsp;  &nbsp; &nbsp;   &nbsp; &nbsp; </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() 阻止事件冒泡。

权限到字段

思路和操作按钮一致,只是这里可以设定几个安全级别:

  1. 宽松:不设置可以访问的列,表示可以使用模块里所有的列 —— 便于设置角色。
  2. 严谨:必须设置可以使用的列,没有设置的话不可以访问 —— 提高安全性。
  3. 预防:敏感列放入“不可访问”名单,可以在一定程度上避免误操作 —— 折中方案。

宽松级别,便于设置角色的权限,因为大部分情况下,可以使用这个模块的话,那么就意味着模块里的列都是可以访问的,如果必须设置列,那么有点繁琐,还容易点错。

严谨级别是对于要求严格的项目而设置,为了更安全,必须设置可以访问的列。虽然更安全,但是显然做设置的时候比较繁琐。

预防级别,这是一个折中的方案,既然大部分字段都可以访问,只有个别的不能访问,那么我们把这几个敏感字段标注起来,列入“黑名单”。

  <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 没有报错,否则就麻烦了。

101权限到字段.png

资源权限

资源权限,主要是后端的事情,因为前端不应该拿到没有权限的数据。

简单的说呢,非常简单,我们首先看一下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。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改