系统权限控制插件封装-实现系统权限控制插件化

        背景:按照传统的开发方式方式,每次新开发一个系统,就需要花费大量时间精力去搭建权限控制模块,如果我们把权限控制这一整个模块都抽离成一个独立的权限控制插件,支持单命令安装,全面暴露参数与方法,就可以通过配置快速集成完整的权限控制机制。

        意义:便于集成与扩展,提高项目构建速度,减少重复代码,降低工作量。提高开发效率,减少因人工手动搭建导致的不必要的错误。

vivien-permission插件

        这是一个基于后台管理系统中的路由菜单权限控制系统,通过 vue-router 全局控制后台管理系统的菜单权限。

功能

① 能支持单点登录、 Token 维护与路由权限判断
② 提供灵活的配置选项,满足用户个性化需求

使用文档

        该插件的源代码及其使用文档均放在该仓库中。

GitHub - yoguoer/vivien-permissionContribute to yoguoer/vivien-permission development by creating an account on GitHub.icon-default.png?t=N7T8https://github.com/yoguoer/vivien-permission.git

实现原理

        页面/菜单权限实现思路

1、后端权限管理配置

  • 后台系统维护侧边栏目录的配置,包括目录名称、图标、链接等。

  • 后端接口能够返回侧边栏的树形结构数据,这些数据应该包含每个菜单项对应的路由地址和权限标识。

2、前端路由配置

  • 前端项目中定义好静态路由和动态路由的配置。

  • 静态路由通常是那些不需要权限即可访问的页面,如登录页、404页面等。动态路由则是根据用户角色和权限来动态生成的路由。

3、路由匹配与生成

  • 调用后端接口获取侧边栏树形结构数据。前端通过递归遍历后端返回的树形结构数据,并与前端配置的路由进行匹配。

  • 对于匹配成功的路由,将其加入到异步路由表中。

4、路由表整合

  • 将动态生成的异步路由表和静态的常规路由表进行整合。

  • 确保整合后的路由表是完整的,并且按照正确的顺序排列。

5、生成侧边栏菜单

  • 根据整合后的路由表,生成侧边栏菜单的DOM结构。

  • 侧边栏菜单应该包含所有用户有权限访问的菜单项。对于没有权限访问的菜单项,应该进行隐藏或者显示为不可点击状态。

6、路由守卫与权限校验

  • 在前端实现路由守卫,对用户的访问进行权限校验。

  • 当用户尝试访问某个页面时,检查该用户是否具有访问该页面的权限。如果没有权限,则重定向到无权限页面或提示用户。

7、缓存与性能优化

  • 对于一些不经常变动的侧边栏数据,可以考虑使用缓存来提高性能。

  • 在用户登录成功后,可以将侧边栏数据缓存起来,避免重复请求后端接口。

        实现之前,需要先知道一些前置知识,有利于更好地理解。

http://t.csdnimg.cn/4zkwQicon-default.png?t=N7T8http://t.csdnimg.cn/4zkwQ

核心片段

1、登录成功后,获取到token和用户信息,进行存储,然后跳转首页

// 登录方法
const login = async function (params: any) {
  try {
    //添加 try catch 捕获异常
    await userStore.Login(params);
    await userStore.GetUserInfo();
    routerNext();
  } catch (err) {
    console.error(err);
  }
};
接着,进行路由跳转到首页
const routerNext = function () {
  if (router.currentRoute.value.query.redirect) { //如果重新登陆后需要返回原先的路由地址
    router.push(router.currentRoute.value.query.redirect as string);
  } else {
    router.push({ name: "TV_FDS_LIST" });
  }
};

2、在后台权限管理系统根据侧边栏目录配置侧边栏和菜单、前端项目代码配置路由

 3、后端接口返回用户有权限访问的路由表和拥有的权限列表

4、 递归匹配后端路由和前端路由配置,添加路由异步路由表和常规路由表,形成最终的路由表

  • 递归后端接口返回的信息获取用户权限列表的方法:
/**
 * 获取嵌套对象的所有对象的 key 对应 value值
 * @param {*} data 嵌套对象
 * @param {*} arr 存放属性数组
 * @param {*} children 保存嵌套子对象的属性
 * @param {*} key 获取的 value 对应的 key
 * @returns
 */
export function getChildValue(
  data: Array<T> = [],
  arr: Array<T> = [],
  key: string = '',
  children: string = 'children'
) {
  if (!key || data.length <= 0) return
  data.forEach(item => {
    if (item[children]) {
      getChildValue(item.children, arr, key, children)
    }
    arr.push(item[key])
  })
}
    // 获取用户权限列表
    async GetAuthority(getAuthList: Function, domain: string): Promise<T> {
      try {
        if (!getAuthList || typeof getAuthList !== "function") {
          return Error("getAuthList 参数错误")
        }
        const authority: authorityType = {
          menuNames: [], // 菜单权限名称列表
          rule: [],// 按钮级别权限
        }
        /**
         *请求获取路由权限列表,返回对象:
         {
            menuNames: [], // 菜单权限名称列表
            rule: [],// 按钮级别权限
         }
         */
        const data = await getAuthList({
          token: getToken()
        })
        authority.menuNames = data.menuNames
        authority.rule = data.rule
        this.SetAuthority(authority);
        return authority
      } catch (error) {
        this.ClearLocal(domain);
        return null;
      }
    },
  • 前端匹配生成路由的方法:
    // 生成异步路由
    GenerateRoutes(routesMenuNames: Array<RouteItem>, asyncRoutes: AppRouteModule[], basicRoutes: AppRouteModule[]) {
      // 过滤常量路由:过滤没有权限的异步路由
      filterRoutes(basicRoutes, routesMenuNames)
      // 过滤异步路由:过滤没有权限的异步路由
      filterRoutes(asyncRoutes, routesMenuNames)
      this.SetRoutes(asyncRoutes, basicRoutes)
      return asyncRoutes
    },
  • 过滤路由的方法:
/**
 * Filter asynchronous routing tables by recursion
 * 过滤没有权限的常量路由路由:递归前端路由,查找 name 不存在的路由,删除
 * @param routes asyncRoutes
 * @param roles
 */
export function filterRoutes(routesInstans: Array<T>, routesMenuNames: Array<T>): void {
  // 开发环境侧边栏路由不由后端管理系统控制
  // if (process.env.NODE_ENV === envEnum.DEVELOPMENT) return
  // 测试和生产环境下,对常量路由进行过滤
  for (let i = 0; i < routesInstans.length; i++) {
    const route = routesInstans[i]
    if (route.children) {
      filterRoutes(route.children, routesMenuNames)
    }
    if (routesMenuNames && routesMenuNames.length > 0 && (!route?.hidden)) {
      route.hidden = (routesMenuNames.indexOf(route.name) < 0)
    }
  }
}
  • 整合路由表的方法:
    // 设置所有路由
    SetRoutes(asyncFilterRoutes: Array<T>, constantAsyncRoutes: Array<T>) {
      this.routes = constantAsyncRoutes.concat(asyncFilterRoutes).sort((value1: RouteItem, value2: RouteItem) => value1?.order - value2?.order) //所有路由
      this.addRoutes = asyncFilterRoutes //新增异步路由获取后台管理系统路由(前台未设置权限页面,因此异步路由即为后台管理路由)
    },

5、根据生成的路由表设置侧边栏菜单

    // 设置侧边栏路由
    SetRoute(routes: Array<RouteItem>) {
      this.routes = routes
    },
  • 点击某一个主菜单,生成对应侧边栏菜单的方法:

    /**
     * 设置二级菜单显示的路由
     * @param {} param0
     * @param {*} routes 当前路由对象,包含路由名称 name 或则路由路径
     * @returns
     */
    SetShowRouters(routes: RouteItem) {
      const { name, matched } = routes
      let topRouteName = name // 二级路由顶部菜单栏名称
      if (matched && matched.length > 0) { // 根据路由匹配路径获取二级顶部菜单栏名称
        topRouteName = matched[0].name
      }
      const filterRouter = this.routes.map((item: RouteItem) => {
        if (item.name !== topRouteName) {
          item.hidden = true
        } else {
          item.hidden = false
        }
        return item
      })
      this.SetRoute(filterRouter)
      return routes
    }

6、当进行路由跳转时,路由守卫先判断token,没有token且路由地址也不在路由白名单内,就让用户跳转到登录页重新登陆拿token;如果有token,就需要对用户权限进行校验。


import type { Router, RouteItem } from 'vue-router';
import { getToken as toGetToken, getOAToken } from "@/utils/token";
import { routesStoreWithOut } from "@/store/routes";
import { useUserStoreWithOut } from "@/store/user";
import type { AppRouteModule } from "@/types/router";
import { Message as showMsg } from '@/plugin/Message.ts';

const routeStore = routesStoreWithOut();
const userStore = useUserStoreWithOut();

export async function createPermissionGuard(
    router: Router,
    whiteList: string[],
    asyncRoutes: AppRouteModule[],
    basicRoutes: AppRouteModule[],
    getAuthList: Function,
    checkOaLogin: Function,
    domain: string,
    Message: Function
) {
    /**
     * 问题: 直接使用 router.beforeEach 会导致在刷新页面时无法进入 router.beforeEach 的回调函数
     * 原因:可能是因为在刷新页面时,Vue Router 的初始化过程尚未完成,导致路由守卫无法正常触发。
     * 解决方案:将 router.beforeEach 回调函数的逻辑放在一个异步函数中,并在 Vue Router 初始化完成后再调用这个异步函数。你可以使用 router.isReady() 方法来判断 Vue Router 是否已经初始化完成。
     * isReady: isReady(): Promise<void> 返回一个 Promise,它会在路由器完成初始导航之后被解析,也就是说这时所有和初始路由有关联的异步入口钩子和异步组件都已经被解析。如果初始导航已经发生,则该 Promise 会被立刻解析。
     */
    router.isReady().then(() => {

        router.beforeEach(async (to: any, from: any, next: Function) => {
            // 判断用户是否已经登录,已经登录情况下,进入权限判断
            if (toGetToken()) {
                return await routerPermission(to, from, next, whiteList, asyncRoutes, basicRoutes, getAuthList, domain, Message)
            } else {
                // 兼容oa 系统单点登录,获取 oa 中的 token
                const { oaToken } = getOAToken(domain)
                // oa 存在 token,用户已经登录 oa
                if (oaToken) {
                    try {
                        // 使用 oa token 换取当前系统的 token, 登录系统
                        await userStore.CheckOaLogin(checkOaLogin, domain);

                        return next();
                    } catch (err) {
                        userStore.ClearLocal(domain);
                        return next("/login?redirect=" + to.path);

                    }
                    // 用户未登录, 判断是否进入白名单页面路由
                } else if (whiteList.includes(to.name as string)) {
                    return next();
                } else {
                    return next("/login?redirect=" + to.path);
                }
            }

        });
    });

}


/**
 * 路由权限判断函数,根据路由权限进入不同路由
 */
export async function routerPermission(
    to: RouteItem,
    from: RouteItem,
    next: Function,
    whiteList: string[],
    asyncRoutes: AppRouteModule[],
    basicRoutes: AppRouteModule[],
    getAuthList: Function,
    domain: string,
    Message: Function
) {

    // 已经存在 token, 进入用户登录页面
    if (to.path == '/login' && from) {
        // 从登录页面进入,直接进入登录页面
        if (from.path === '/login' || '/') {
            return next();
        } else {
            //已经存在 token, 从其他页面进入用户登录页面,直接返回来源页面
            return next(from.path);
        }
    } else {
        // 获取是否用户权限
        const canAccess = await canUserAccess(to, whiteList, asyncRoutes, basicRoutes, getAuthList, domain)
        if (canAccess) {
            return next()
        } else {
            if (Message) {
                Message({
                    message: "您没有权限访问页面,请联系系统管理员!",
                    type: "warning",
                });
            } else {
                showMsg.error({
                    message: "您没有权限访问页面,请联系系统管理员!",
                });
            }
            return false
        }
    }
}





/**
* 获取异步权限
* @param to 
* @returns 
*/
export async function canUserAccess(
    to: RouteItem,
    whiteList: string[],
    asyncRoutes: AppRouteModule[],
    basicRoutes: AppRouteModule[],
    getAuthList: Function,
    domain: string
) {
    if (!to || to?.name === "Login") return false
    try {
        let accessRoutes = userStore.getAuthority || {}
        if (accessRoutes?.menuNames && accessRoutes?.menuNames?.length === 0) {
            // 获取用户异步路由权限
            accessRoutes = await userStore.GetAuthority(getAuthList, domain)
            // 生成用户所有路由权限
            routeStore.GenerateRoutes(accessRoutes?.menuNames || [], asyncRoutes, basicRoutes)
        }
        const allRoutes = [...whiteList, ...accessRoutes?.menuNames]
        return allRoutes.length > 0 && allRoutes.includes(to.name)
    } catch (err) {
        userStore.Logout(domain)
        return false
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/619348.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【算法】竞赛常用知识之字符串1

前言&#xff1a; 本系列是学习了董晓老师所讲的知识点做的笔记 董晓算法的个人空间-董晓算法个人主页-哔哩哔哩视频 (bilibili.com) 动态规划系列&#xff08;还没学完&#xff09; 【算法】动态规划之线性DP问题-CSDN博客 【算法】动态规划之背包DP问题&#xff08;2024…

Linux中云盘/磁盘,爆满处理方式

1&#xff1a;du和df命令查看磁盘大小不一致 以下是阿里云服务器云盘使用率 运行 du -sh / 大小为20g 我的服务器大小为40g 按道理说这个云盘使用率应该是百分之五十 而运行 df -h / 这个命令是跟这个云盘使用率差不多的。 1.1分析原因 我安装了mysql&#xff0c;nginx…

47岁古天乐唯一承认女友约「御用阿妈」过母亲节

日前关宝慧在IG晒出一张聚会照&#xff0c;并写道&#xff1a;「预祝各位#母亲节快乐&#x1f339;#dinner #happy #friends #好味」相中所见&#xff0c;前TVB金牌监制潘嘉德、卢宛茵、黄&#x28948;莹、黎萨达姆都有出席饭局。 当中黄&#x28948;莹身穿卡其色西装褛&…

从“制造”到“智造”:“灯塔”经验助力中国制造业转型升级-转载

作者&#xff1a;Karel Eloot&#xff0c;侯文皓&#xff0c;Francisco Betti&#xff0c;Enno de Boer和Yves Giraud 作为中国实体经济的主体&#xff0c;制造业是推动中国经济发展乃至全球制造业持续增长的重要引擎。站在历史与未来交汇的新起点上&#xff0c;中国制造业将背…

ERP与MES与WMS集成

WMS储位管理 WMS与MES集成 (一) 打通追溯链 在拣货时&#xff0c;将配料标签与供应商的物料标签进行关联。通过配料标签达到精确追溯及防错目的。针对模糊查询&#xff0c;将工单与物料的供应商信息、仓库流转信息进行关联。 (二) WMS入库 成品(半成品)下线后&#xff0c;M…

MySQL查询篇-聚合函数-窗口函数

文章目录 distinct 关键字聚合函数常见的聚合函数group by和having 分组过滤 窗口函数with as窗口聚合函数排名窗口函数值窗口函数 distinct 关键字 distinct 去重数据&#xff0c;ps:null值也会查出来 select distinct column from table;聚合函数 常见的聚合函数 select …

【前端系列】什么是yarn

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

浅谈@Controller注解和其他四大注解的区别

各位大佬光临寒舍&#xff0c;希望各位能赏脸给个三连&#xff0c;谢谢各位大佬了&#xff01;&#xff01;&#xff01; 目录 1.Spring五大注解的使用约定 2.Controller注解的特别之处 3.总结 1.Spring五大注解的使用约定 Spring的五大注解&#xff08;Controller&#x…

【无标题】能效?性能?一个关于openssl speed速度测试的诡异问题。

问题描述 最近的某个软件用到了openssl&#xff0c;所以就想着测试一下速度。我的电脑是惠普的&#xff0c;CPU是AMD Ryzen 7 PRO 6850HS&#xff0c;系统是Win11。我使用openssl自带的speed测试加密/解密的速度&#xff0c;命令大致如下&#xff1a; openssl speed -evp aes…

python数据分析——matplotlib可视化基础

参考资料&#xff1a;活用pandas库 # 导入库 import pandas as pd import matplotlib.pyplot as plt # 导入数据 anscombepd.read_csv(r"...\seaborn常用数据案例\anscombe.csv") anscombe.head() 大多数基本图表的名字以plt.plot开头。 # 创建数据子集 # 只包含数…

电力场景设备漏油检测数据集VOC+YOLO格式338张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;338 标注数量(xml文件个数)&#xff1a;338 标注数量(txt文件个数)&#xff1a;338 标注类别…

linux学习:视频输入+V4L2

目录 V4L2 视频采集流程 代码例子 核心命令字和结构体 VIDIOC_ENUM_FMT VIDIOC_G_FMT / VIDIOC_S_FMT / VIDIOC_TRY_FM VIDIOC_REQBUFS VIDIOC_QUERYBUF VIDIOC_QBUF /VIDIOC_DQBUF VIDIOC_STREAMON / VIDIOC_STREAMOFF V4L2 是 Linux 处理视频的最新标准代码模块&…

Hadoop3.4.0 完全分布式集群 运行环境搭建 VMware Workstation 虚拟机 大数据系列 一

一 生产环境集群模式部署&#xff0c;需要多台主机&#xff0c;主机之间通过密钥相互访问. 1 配置如图 节点名字节点IP系统版本master11192.168.50.11centos 8.5slave12192.168.50.12centos 8.5slave13192.168.50.13centos 8.5 2 安装服务器 #先安装一台master11&#xff…

读人工智能时代与人类未来笔记01_重塑人类社会秩序

1. AlphaZero 1.1. 2017年年底&#xff0c;由谷歌旗下DeepMind公司开发的人工智能程序AlphaZero击败了当时世界上最强大的国际象棋程序Stockfish 1.1.1. AlphaZero对Stockfish的百场战绩是28胜72平0负&#xff0c;可以说获得了压倒性的胜利 1.1.2. …

手撕C语言题典——反转链表

目录 前言 一.思路 1&#xff09;创建新链表 2&#xff09;创建三个指针 二.代码实现 搭配食用更佳哦~~ 数据结构之单单单——链表-CSDN博客 数据结构之单链表的基本操作-CSDN博客 前面学了单链表的相关知识&#xff0c;我们来尝试做一下关于顺序表的经典算法题~ 前言 反转…

RocketMQ(一)

作用 1. 限流削峰 2. 异步解耦 组成 Producer&#xff1a;消息的发送者&#xff0c;生产者&#xff1b;举例&#xff1a;发件人 Consumer&#xff1a;消息接收者&#xff0c;消费者&#xff1b;举例&#xff1a;收件人 Broker&#xff1a;暂存和传输消息的通道&#xff1…

C语言 | Leetcode C语言题解之第85题最大矩形

题目&#xff1a; 题解&#xff1a; int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize) {int m matrixSize;if (m 0) {return 0;}int n matrixColSize[0];int left[m][n];memset(left, 0, sizeof(left));for (int i 0; i < m; i) {for (int j …

---随笔--Java实现TCP通信(双端通信接收与发送)

---随笔--Java实现TCP通信&#xff08;双端通信接收与发送&#xff09; 引言1. 什么是TCP通信2. 服务器与客户端核心代码2.1 服务器ServerSocket端核心代码2.2 用户Socket端核心代码2.3 小贴士之关于try-with-resources自动关闭资源的使用 3. 具体服务器端实现4. 具体客户端实现…

LLM量化

Efficient Finetuning prefix tuning 针对每种任务&#xff0c;学习prefix vector 启发于prompting&#xff0c;调整上下文内容让模型去输出自己想要的内容 核心就是找到一个上下文去引导模型解决NLP生成任务 传统情况下&#xff0c;我们为了得到想要的结果&#xff0c;会…

jdk8的新特征

1&#xff1a; jdk8中新增的方法 在jdk8中对接口进行了增强&#xff0c;在jdk8之前 interface 接口名{ 静态常量&#xff1a; 抽象方法&#xff1a; } 在jdk8之后 interface 接口名{ 静态常量&#xff1a; 抽象方法&#xff1a; 默认方法&#xff1a; 静态方法&#xff1a; } 2…