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

news2024/11/16 7:39:11

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

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

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.coloradmin.cn/o/1656492.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux Ubuntu(玩客云) qBittorrent docker BT下载(qbittorrent 密码错误无法登录 ip地址被禁止登录等)

提示&#xff1a; 需要提前安装Docker 根据qBittorrent官网的更新日志https://www.qbittorrent.org/news &#xff0c;4.6.1.0包含一个重大更新。可以看到自4.6.1.0开始&#xff0c;qBittorrent将弃用adminadmin默认密码&#xff0c;采用随机密码&#xff0c;将在终端控制台输出…

伦敦银软件下载完成后如何开始交易?

在伦敦金投资的整个流程中&#xff0c;进行伦敦银软件的下载可以说也是重要的一步。伦敦银软件是由交易平台提供的&#xff0c;也是交易服务的具体体现。没有平台&#xff0c;我们就不能下单&#xff0c;也不能入场。那么&#xff0c;伦敦银软件下载完成后如何开始交易呢&#…

找不到msvcr120.dll无法继续执行

windows&#xff08;新安装的系统&#xff09;安装mysql&#xff0c;报错MSVCR120.dll找不到 官方下载地址 https://www.microsoft.com/zh-CN/download/details.aspx?id40784&wd&eqid9eba4d380059694e00000004658ce260 安装上就好了

Spring AI实战之一:快速体验(OpenAI)

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 关于Spring AI Spring Boot、Spring Cloud、Spring Data&#xff0c;作为一名Java程序员&#xff0c;相信您对这些概览早已耳熟能详&#xff0c;或者天天在用…

1802907-91-0,甲基四嗪PEG4羧酸一种胺反应试剂

基本信息&#xff1a; 中文名称&#xff1a;甲基四嗪-四聚乙二醇-羧基&#xff0c;甲基四嗪PEG4羧酸 英文名称&#xff1a;Methyltetrazine-PEG4-acid&#xff0c;Methyltetrazine-PEG4-COOH CAS号&#xff1a;1802907-91-0 分子式&#xff1a;C20H28N4O7 分子量&#xff…

双翻斗雨量计学习

双翻斗雨量计用户手册&#xff08;脉冲型&#xff09; 本仪器由雨量计壳体、承雨口、漏斗、翻斗支撑、上漏斗雨量调节支架、上漏斗、汇集漏斗、计数翻斗雨量调节支架、计数翻斗、干簧管安装架、轴承螺钉、出水漏斗、腿部支架、干簧管、水平泡、调节支撑板、控制盒、调平装置、接…

我国破碎筛分设备市场规模逐渐扩大 砂石骨料为主要应用领域

我国破碎筛分设备市场规模逐渐扩大 砂石骨料为主要应用领域 破碎筛分设备是将物料进行破碎分级&#xff0c;并按照一定规格进行筛分的机械设备。破碎筛分设备具有高效率、移动速度快、节省人力等优点。根据配置组合不同&#xff0c;破碎筛分设备可分为辊式、颚式、圆锥式、旋回…

【C】语⾔内存函数--超详解

1. memcpy 使⽤和模拟实现 void * memcpy ( void * destination, const void * source, size_t num ); 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。 这个函数在遇到 \0 的时候并不会停下来。 如果source和destination有任何的重叠&am…

若依框架dialog弹窗取消点击空白出关闭

如果想全局取消的话就找到main.js在里面加上下面的一行代码&#xff0c;添加完成之后记得清楚浏览器缓存重新加载js文件。 Element.Dialog.props.closeOnClickModal.default false;如果想指定某个弹窗取消点击空白处关闭&#xff0c;那么就找到那个弹窗加上。添加完毕之后刷新…

python对私钥或者Cookie设置和读取环境变量

1. 开发环境 MAC 2. 设置python脚本中的私钥或者cookie的环境变量 vim ~/.bash_profile 打开文件 export COOKIEmy_cookie 设置环境变量 执行source ~/.bash_profile 3.读取环境变量 在pycharm中创建test_env.py文件 import os# 获取环境变量 api_key os.getenv(COOKI…

Java设计模式 _行为型模式_责任链模式

一、责任链模式 1、责任链模式 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型模式。该模式会为请求创建一个接收者对象的链。在这个责任链里&#xff0c;通常每个接收者都包含对另一个接收者的引用&#xff0c;如果一个对象不能处理该请求&…

Linux运维:centos环境变量

前言 在 Linux 运维工作中&#xff0c;管理环境变量是至关重要的一项任务。在 CentOS 环境下&#xff0c;正确配置环境变量可以使系统更加高效和易于管理。 本文将重点讨论 CentOS 环境下的环境变量设置&#xff0c;并就python的环境变量配置方案进行讲解&#xff08;不包含Ano…

汽车软件研发工具链丨怿星科技新产品重磅发布

“创新引领未来”聚焦汽车软件新基建&#xff0c;4月27日下午&#xff0c;怿星科技2024新产品发布会在北京圆满举行&#xff01;智能汽车领域的企业代表、知名大企业负责人、投资机构代表、研究机构代表齐聚现场&#xff0c;线上直播同步开启&#xff0c;共同见证怿星科技从单点…

ubuntu下载zotero

https://www.zotero.org/download/ 下载完安装包之后&#xff0c;打开到这个位置右键点击打开终端 打开终端输入 ./zotero 随后进入到zotero弹出窗口直接next安装即可

(1)AB_PLC Studio 5000软件与固件版本升级

AB_PLC Studio 5000软件与固件版本升级 1. 软件版本升级2. 固件版本升级1. 软件版本升级 使用将老程序从19版本升级到33版本。 step1:双击程序.ACD文件,打开界面如下。 step2:点击更改Controller,选择我们的新CPU的型号和版本号。点击确定 step3:点击确定,等待。 st…

21物联1班shift五次

1.选择推荐选项 2.等待 3.点击取消 4.选择查看问题详细信息 5.点击txt文件 6.找到system文件夹&#xff0c;将sethc改为qqq&#xff0c;将cmd文件改为sethc文件 7.单击完成。重新启动虚拟机。连续按五次shift出现cmd框&#xff0c;修改密码

【解疑】ZIP分卷压缩文件如何设置和取消密码?

压缩大文件&#xff0c;我们可以设置压缩成ZIP分卷文件&#xff0c;这样更利于传输和存储。如果分卷文件比较重要&#xff0c;还可以设置密码保护&#xff0c;那ZIP分卷压缩文件的密码如何设置和取消呢&#xff1f;下面一起来看看吧&#xff01; 设置ZIP分卷密码&#xff1a; …

AI时代:人工智能大模型引领科技创造新时代

目录 前言一. AI在国家战略中有着举足轻重的地位1.1 战略1.2 能源1.3 教育 二. AI在日常生活中扮演着重要角色2.1 医疗保健2.2 智能客服2.3 自动驾驶2.4 娱乐和媒体2.5 智能家居 三. AI的未来发展趋势 总结 前言 随着AI技术的进步&#xff0c;新一代的AI技术已经开始尝试摆脱依…

买入期权是什么意思?

今天期权懂带你了解买入期权是什么意思&#xff1f;买入期权&#xff1a;也称看涨期权。一种赋予其持有者以特定的价格、在特定的到期日当天或之前买入某种资产的权力的金融工具。 买入期权是什么意思&#xff1f; 买入期权也称看涨期权。即赋予其持有者在到期日或到期日之前按…

Java | Leetcode Java题解之第71题简化路径

题目&#xff1a; 题解&#xff1a; class Solution {public String simplifyPath(String path) {String[] names path.split("/");Deque<String> stack new ArrayDeque<String>();for (String name : names) {if ("..".equals(name)) {if …