你真的了解项目中的权限控制吗?

426 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

前言

作为一个天天写JS的前端,工作中经常会用到权限控制,可能你也有点小心得,最近在项目上参与权限相关设计,所以抽空记录一下权限相关,我们一起来做个总结和复盘。

权限分类

  • 用户权限
  • 数据权限
  • 按钮权限
  • 接口权限
  1. 对于用户权限,这个一般管理类系统都具备,就是典型的RBAC模型
  2. 数据权限,这个基本都是后端服务或者数据模型来进行控制,比较麻烦,而且有专门一套理论
  3. 页面显示层的交互操作,如按钮的显示和隐藏
  4. 更高级别的权限控制,主要控制页面内的请求,跨功能页面或者外部直接访问,都属于非法访问

image.png

RBAC模型

这个名词其实很多人都不清楚,其实我们日常使用的权限解决方案,就是采用的RBAC模型,

我们来看看下面的介绍

含义:RBAC模型(Role-Based Access Control:基于角色的访问控制)

三个基础组成部分,分别是用户、角色和权限

User(用户):每个用户都有唯一的UID识别,并被授予不同的角色;

Role(角色):不同角色具有不同权限;

Permission(权限):访问权限;

image.png

只是最简单的模型,当然还有更复杂的约束性更强的,RBAC0、RBAC1、RBAC2

RBAC0 用户和角色之间可以是多对多的关系

RBAC1 角色间的继承关系,即角色上有了上下级的区别

RBAC2 基于RBAC0模型的基础上,进行了角色的访问控制,有了互斥角色、级别等概念,就像我们生活中,国家主席是从副主席中选举的一样;

更详情大家可以去找相关文献研究下,理论的知识还是需要填充,不然面试的时候就弱鸡了,

具体实践都是各种窜,完全根据产品思维游动

点题部分讲解完毕,下来我们讲解按钮权限

按钮权限

一般的系统对于交互类权限控制都会有比较精细化管理,主要是针对不同的用户角色来进行判断

  • 管理类角色:对CRUD交互,就有添加、编辑、删除这类操作
  • 普通用户角色:对CRUD交互,有查询的操作权限

我们的系统一般围绕这两大类角色进行,当然这类角色会衍生出一些其他的角色,就看需要根据业务特征进行。

权限流程

image.png

从上图可以看出,用户登录后,从用户信息中提取按钮权限信息,然后存放于全局缓存中,页面加载时,对相应按钮进行逻辑处理即可

实现方式

v-if 逻辑处理法

<a title="删除" v-if="isDel" @click="delHandel(node, data)" class="del">删除</a>

const isDel = storage.get('authKey')

这种方法暴力直接,好多项目都是这么干的,如果出现多重逻辑判断,加上三元运算符,复杂度会成倍的增加,变成屎山级代码,我来找找反例代码:

image.png

组件控制法

<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>

这两种使用方式不一样,一个加了模块前缀,一个没有添加

  • 对于添加模块前缀的,这个相对来说自由一些,可以跨模块的处理
  • 不添加模块前缀,这个就必须是在某个模块下进行,这样就对权限控制,页面控制就更加精细。

实际的情况,解决方案的深度挖掘以及安全策略,还是根据架构设计的策略走吧。
不然你可能会被打死的哟

接口权限

image.png

上述得流程,是针对页面级别下挂载得请求接口

也可以和上节讲述的按钮权限一样,登录后当前用户的接口权限映射列表进行缓存,发起请求后,在进行全局查找配置,如果存在就通过,不存在即没有权限。

  • 对于页面级别,这个需要在进入页面前,进行接口列表加载。这个可以与后端进行约定
  • 页面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()
        }
    });
}

亲爱的你,好好记住这些方式和理论,是你实际解决问题的宝器....

加油吧,老铁!!