携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
前言
作为一个天天写JS的前端,工作中经常会用到权限控制,可能你也有点小心得,最近在项目上参与权限相关设计,所以抽空记录一下权限相关,我们一起来做个总结和复盘。
权限分类
- 用户权限
- 数据权限
- 按钮权限
- 接口权限
- 对于用户权限,这个一般管理类系统都具备,就是典型的RBAC模型
- 数据权限,这个基本都是后端服务或者数据模型来进行控制,比较麻烦,而且有专门一套理论
- 页面显示层的交互操作,如按钮的显示和隐藏
- 更高级别的权限控制,主要控制页面内的请求,跨功能页面或者外部直接访问,都属于非法访问
RBAC模型
这个名词其实很多人都不清楚,其实我们日常使用的权限解决方案,就是采用的RBAC模型,
我们来看看下面的介绍
含义:RBAC模型(Role-Based Access Control:基于角色的访问控制)
三个基础组成部分,分别是用户、角色和权限
User(用户):每个用户都有唯一的UID识别,并被授予不同的角色;
Role(角色):不同角色具有不同权限;
Permission(权限):访问权限;
只是最简单的模型,当然还有更复杂的约束性更强的,RBAC0、RBAC1、RBAC2
RBAC0
用户和角色之间可以是多对多的关系
RBAC1
角色间的继承关系,即角色上有了上下级的区别
RBAC2
基于RBAC0模型的基础上,进行了角色的访问控制,有了互斥角色、级别等概念,就像我们生活中,国家主席是从副主席中选举的一样;
更详情大家可以去找相关文献研究下,理论的知识还是需要填充,不然面试的时候就弱鸡了,
具体实践都是各种窜,完全根据产品思维游动
点题部分讲解完毕,下来我们讲解按钮权限
按钮权限
一般的系统对于交互类权限控制都会有比较精细化管理,主要是针对不同的用户角色来进行判断
- 管理类角色:对CRUD交互,就有添加、编辑、删除这类操作
- 普通用户角色:对CRUD交互,有查询的操作权限
我们的系统一般围绕这两大类角色进行,当然这类角色会衍生出一些其他的角色,就看需要根据业务特征进行。
权限流程
从上图可以看出,用户登录后,从用户信息中提取按钮权限信息,然后存放于全局缓存中,页面加载时,对相应按钮进行逻辑处理即可
实现方式
v-if 逻辑处理法
<a title="删除" v-if="isDel" @click="delHandel(node, data)" class="del">删除</a>
const isDel = storage.get('authKey')
这种方法暴力直接,好多项目都是这么干的,如果出现多重逻辑判断,加上三元运算符,复杂度会成倍的增加,变成屎山级代码,我来找找反例代码:
组件控制法
<AuthComponent>
<a title="删除" @click="delHandel(node, data)" class="del">删除</a>
</AuthComponent>
React版本
import React from "react";
const { BaseStore } = window;
class AuthComponent extends React.Component {
render() {
let { actionName, noAuthContent } = this.props;
let action = BaseStore.menu.isAuth(actionName); //获取按钮权限
if (!action) {
return noAuthContent ? noAuthContent : null;
} else {
return React.cloneElement(this.props.children);
}
}
}
export default AuthComponent;
Vue版本
<template>
<span v-if="isExternal" >
<slot></slot>
</span>
</template>
<script setup>
const props = defineProps({
actionName: {
type: String,
required: true
}
})
let action = BaseStore.menu.isAuth(actionName); //获取按钮权限
</script>
这种方法,相对来说,权限逻辑会封闭在公共组件中,也可以采纳。
这种封装方式,从语言特性上看更偏向与React、JSX类的语法使用,但对于Vue使用起来感觉相对还是有点蹩脚。
指令控制法
index.js
/*
* @description: 指令管理-权限
*/
import auth from './permission'
async function install(app) {
app.directive('auth', auth)
}
export default {
install
}
permission.js
/**
* v-auth 操作权限处理
*/
import { BaseStore } from '@basic-library'
import { useHistory } from '@hooks'
export default {
mounted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const cUrl = useHistory().currentRoute.value.path
const permissions = BaseStore.auth.getAuthPrivilege(cUrl) || []
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
if(!permissions){
throw new Error(`查询操作权限标签值为空!`)
}
const hasPermissions = permissions.split(":").some(permission => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}
上述的方式,是基于Vue的指令系统,自定义指令扩展
在Vue使用中相对来说,是一种比较好的方式
- 开发人员只需要与服务端约定好按钮权限的编码
- 使用起来非常简洁
<a title="添加" v-auth="['add']" @click="addHandel(data)" class="add">添加</a>
研究上面的代码,优秀的你可能发现了一点
const cUrl = useHistory().currentRoute.value.path
const permissions = BaseStore.auth.getAuthPrivilege(cUrl) || []
获取当前页面访问地址,然后根据地址,在全局缓存的对象中查找当前地址下所拥有的权限编码
权限编码集合存储在全局这没有问题,但是你要充分考虑开发人员的使用方式:
<a title="添加" v-auth="['module.add']" @click="addHandel(data)" class="add">添加</a>
<a title="添加" v-auth="['add']" @click="addHandel(data)" class="add">添加</a>
这两种使用方式不一样,一个加了模块前缀,一个没有添加
- 对于添加模块前缀的,这个相对来说自由一些,可以跨模块的处理
- 不添加模块前缀,这个就必须是在某个模块下进行,这样就对权限控制,页面控制就更加精细。
实际的情况,解决方案的深度挖掘以及安全策略,还是根据架构设计的策略走吧。
不然你可能会被打死的哟
接口权限
上述得流程,是针对页面级别下挂载得请求接口
也可以和上节讲述的按钮权限一样,登录后当前用户的接口权限映射列表进行缓存,发起请求后,在进行全局查找配置,如果存在就通过,不存在即没有权限。
- 对于页面级别,这个需要在进入页面前,进行接口列表加载。这个可以与后端进行约定
- 页面ID,返回当前页面得权限接口列表,然后在把映射列表注册到全局得接口管理器中
- 页面操作发起请求后,根据权限编码去接口管理器中查找匹配,如果存在就继续请求,如果不存在就进行提醒
实现代码:
/**
* registerPageMount 全局页面挂载监听
* @param {*} router 路由
* 场景:
* 1、页面内获取接口请求标记-内置全局实例
* 2、页面内错误捕获
*/
function registerPageMount(router){
// 进入页面
router.beforeResolve(async(to, from, next) => {
document.title = Config.BSConfig.systemName || '';
try {
if(Config.BSConfig?.IS_AUTH_INTERFACE){
const cItem= BaseStore.auth.getInfoByRoute(to.path)
if(cItem){
const result = await Service.useHttp('interfaceService',`pageId=${cItem.pageId}`)
cache.setCache('_EDU_CUR_PAGE_INFO',cItem,'session')
if(result?.success){
cache.setCache('_EDU_CUR_INTERFACE_INFO',result?.returnObj,'session')
Service.initData(result?.returnObj)
}else{
console.warn(`当前页面${cItem.pageId}接口未授权!`)
}
}
}
next()
} catch (error) {
console.warn(error)
next()
}
});
}
亲爱的你,好好记住这些方式和理论,是你实际解决问题的宝器....
加油吧,老铁!!