使用React Router实现前端的权限访问控制

前段时间学习了React Router,发现没有Vue里面的路由功能强大,没有直接提供路由中间件,不能像Vue里面一样在路由配置上设置任意的额外属性,但是可以通过一些技巧来实现这些功能。

1、配置菜单


后台管理系统一般都会在左侧显示菜单,右侧显示页面,本例中使用Ant Design组件当然也不例外。虽然umi里面已经集成了很多功能,但是有些地方用起来不够灵活,比如路由配置高阶组件,不能传递prop;每一个权限码都要配置相同的函数,等等。所以,我更喜欢用Vite+React来搭建项目。
废话不多说,菜单配置的代码如下

export type MenuConfig = {computedMatch?: match<any>;route?: MenuDataItemlocation: {pathname?: string;};
}export type MenuDataItem = {/** @name 子菜单 */children?: MenuDataItem[];routes?: MenuDataItem[];/** @name 在菜单中隐藏子节点 */hideChildrenInMenu?: boolean;/** @name 在菜单中隐藏自己和子节点 */hideInMenu?: boolean;/** @name 菜单的icon */icon?: React.ReactNode;/** @name 自定义菜单的国际化 key */locale?: string | false;/** @name 菜单的名字 */name?: string;/** @name 用于标定选中的值,默认是 path */key?: string;/** @name disable 菜单选项 */disabled?: boolean;/** @name disable menu 的 tooltip 菜单选项 */disabledTooltip?: boolean;/** @name 路径,可以设定为网页链接 */path?: string;/*** 当此节点被选中的时候也会选中 parentKeys 的节点** @name 自定义父节点*/parentKeys?: string[];/** @name 隐藏自己,并且将子节点提升到与自己平级 */flatMenu?: boolean;/** @name 指定外链打开形式,同a标签 */target?: string;/*** menuItem 的 tooltip 显示的路径*/tooltip?: string;/*** 组件*/component?: Promise<{ default: React.ComponentType<any> }>;/*** 权限码*/access?: string;}const menuConfig: MenuConfig = {route: {path: '/',routes: [{key: '1',name: '首页',path: '/home',icon: <HomeFilled />,component: import('@/pages/home')},{name: '系统管理',path: '/system',access: 'system:view',icon: <SettingFilled />,routes: [{name: '用户管理',path: '/system/user',icon: <ContactsFilled />,access: 'user:view',component: import('@/pages/system/user')},{name: '角色管理',path: '/system/role',icon: <SmileFilled />,access: 'role:view',component: import('@/pages/system/role')},{name: '权限管理',path: '/system/authority',access: 'ahthority:view',routes: [{name: '菜单按钮管理',path: '/system/authority/menu',icon: <GoldenFilled />,access: 'menuBtn:view',component: import('@/pages/system/authority/menuBtn')},{name: '接口权限管理',path: '/system/authority/interface',icon: <SecurityScanFilled />,access: 'interface:view',component: import('@/pages/system/authority/interface')}]}]}],},location: {pathname: '/',}
}export default menuConfig

这里使用import()函数,动态导入组件,避免将来路由组件多了以后,在开头写大量的import语句,access表示权限码,用来控制菜单的隐藏和显示,并且可以将权限码传递给路由,设置路由的访问权限,后面会说到。

2、定义用户的全局状态

export type UserInfo = {userName?: string | null,avatar?: string | null,authCodes?: Set<string> | null,
}const userInfo: UserInfo = {userName: '',avatar: '',authCodes: undefined,
}const user = {data: userInfo,async requestUserInfo() {const userInfoFromDB = await getUserInfo()this.data = { ...userInfoFromDB, authCodes: new Set(userInfoFromDB.menuBtnCodes) }localStorage.setItem(USER_INFO,JSON.stringify(userInfoFromDB))}
}export default {user,tab,menu
}

这里将用户的用户名、头像、权限码,保存到了全局变量当中,其中authCodes代表权限码的Set集合,里面包含了菜单和按钮的权限码,方便后面进行校验,requestUserInfo函数用来请求后台接口,获取用户信息,并保存到全局变量和本地缓存当中,然后将user对象导出,tab和menu涉及到其他的功能,这里先不讨论。

3、获取用户信息

为主页所在的路由组件定义一个函数,作为loader,并在loader函数里面获取用户信息

export const loader = async ({request}:LoaderFunctionArgs) => {const url=new URL(request.url)if (url.pathname==import.meta.env.VITE_BASE_NAME) {return redirect('/home')}if (currentAction==Action.INIT) {await store.user.requestUserInfo()store.menu.filterMenuConfig()}else{currentAction=Action.INIT}return { userInfo: store.user.data, tabsData: store.tab.data,menuConfig:store.menu.data }
}

其中,store对象保存了当前的全局状态,调用requestUserInfo()函数请求后台获取用户信息,并保存,然后需要将store对象里面的数据返回。代码中的其他逻辑涉及其他功能,这里先不讨论。
然后,使用useLoaderData()函数,在主页的路由组件里面获取到这些信息即可,后面可以进行显示和调用。
pro components组件库有很多高级组件,只要调用ProLayout组件,将loader获取的数据传入对应的prop即可,由于代码量庞大,这里不展开讨论。

const loaderData = useLoaderData() as { userInfo: UserInfo, tabsData: TabsData,menuConfig:MenuConfig}
const {userInfo,tabsData,menuConfig}=loaderData

4、定义权限校验函数

/*** 检查权限* @param access 权限码* @returns true 有权限*          false 没有权限*/
export function checkAuth(access?:string){if (access) {const authCodes=store.user.data.authCodesif (!authCodes) {const userStr=localStorage.getItem(USER_INFO)if (userStr) {const userInfo:SYSTEM_API.UserInfo =JSON.parse(userStr)const menuCodes=new Set(userInfo.menuBtnCodes)return checkAccess(access,menuCodes)}else{store.user.requestUserInfo().then(() => {const menuCodes=store.user.data.authCodescheckAccess(access,menuCodes)})}}else{return checkAccess(access,authCodes)}}else{return true}
}function checkAccess(access:string,authCodes?:Set<string>|null){if (authCodes?.has(access)) {return true}else{return false}
}

checkAuth是一个权限校验的函数,首先从全局状态当中获取用户权限码,如果为空,就从本地缓存中获取,如果本地缓存为空,就请求后台去获取,然后判断权限码的Set集合里面是否包含当前所需权限,返回true代表验证通过,返回false代表没有权限。

5、生成路由

let key=1const createRoutes = (menus: MenuDataItem[] | undefined) => {if (menus) {const routes: RouteObject[] = []for (const menu of menus) {if (menu.path) {const route: RouteObject = {path: menu.path}if (menu.component) {const module = menu.componentconst Component = React.lazy(() => module)route.element=(<Suspense fallback={<ProSkeleton type="list"></ProSkeleton>}><MiddleWare tabKey={String(key)} title={menu.name} path={menu.path}><Component /></MiddleWare></Suspense>)menu.key=String(key)key++}else if (!menu.routes && !menu.children) {route.element=(<MiddleWare tabKey={String(key)} title={menu.name} path={menu.path}></MiddleWare>)menu.key=String(key)key++}const children = createRoutes(menu.routes || menu.children)if (children) {route.children = children}route.loader=() => {if (!checkAuth(menu.access)) {throw new Response('Forbidden',{status:403})}return null}routes.push(route)}}return routes}
}const menus = menuConfig.route?.routesconst routes = createRoutes(menus);export {menuConfig}const router = createBrowserRouter([{path: '/',element: <Main />,loader: mainLoader,action:mainAction,errorElement:<ErrorBoundary/>,children: [...(routes || [])]},{path: '/login',element: <Login />,errorElement:<ErrorBoundary/>},
], {basename: import.meta.env.VITE_BASE_NAME
})export default router

这里面的逻辑比较复杂。
createRoutes是一个递归函数,用来循环递归遍历菜单,通过调用React.lazy()函数得到菜单中的组件对象,使用Suspense组件进行包裹才能正常显示。MiddleWare是我自定义的一个高阶组件,用来获取菜单信息,控制tab页的显示状态,这里不展开讨论。这里还为菜单对应的路由创建了loader,在loader函数里面调用前面定义的checkAuth,判断是否有权限访问对应的路由,如果没有权限,就抛出异常,显示错误页,也就是403页面。
在createBrowserRouter函数里面配置了主页和登录页的错误页,并将前面定义的loader函数在主页的路由配置当中进行配置,用于获取用户信息,将菜单生成的路由在主页的children配置中展开。
当然,也要在main.tsx中调用路由对象,才能使它生效

createRoot(document.getElementById('root')!).render(<StrictMode><RouterProvider router={router}/></StrictMode>,
)

6、菜单权限校验

//检查菜单的权限,并做一次深拷贝,得到新的对象
function filterMenuConfig(){const menus=menuConfig.route?.routesif (menus) {const menuConfigCopy:MenuConfig={route:{path:'/',routes:filterMenus(menus)},location:{pathname:'/'}}menu.data=menuConfigCopy}
}function filterMenus(menus: MenuDataItem[]) {const menusCopy:MenuDataItem[]=[]for (const menu of menus) {const menuCopy={...menu}menusCopy.push(menuCopy)if (!checkAuth(menuCopy.access)) {menuCopy.hideInMenu = true}const children = menuCopy.routes || menuCopy.childrenif (children) {menuCopy.routes=filterMenus(children)}}return menusCopy
}const menu = {data: menuConfig,filterMenuConfig
}export default {user,tab,menu
}

这里定义了filterMenuConfig函数,对菜单的配置对象做了一次深拷贝,通过循环遍历和递归拷贝了里面的每一个对象,并且在这过程中调用前面定义的checkAuth,来检查每个菜单的权限,如果没有权限,就隐藏对应的菜单。之所以要做深拷贝,就是为了不破坏原先菜单配置里面的数据,方面用户退出的时候恢复菜单数据。
然后,可以在主页的loader里面调用filterMenuConfig函数,代码如第3步所示。

7、按钮权限校验

export function Access({children,auth}:{children?:ReactNodeauth?:string
}){if (checkAuth(auth)) {return (<>{children}</>)}
}

这里定义了一个高阶组件,用于对按钮的权限进行校验,组件内调用了前面定义的checkAuth函数,如果用户没有权限,就不会显示对应的按钮。
调用示例如下

<Access auth="user:save"><Button type="primary" icon={<PlusCircleOutlined />} onClick={() => {dialogRef.current?.openDialog('新增用户')}}>新增</Button>
</Access>

其中,user:save代码按钮的权限码,只有拥有这个权限的用户才能看到这个按钮。

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

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

相关文章

“链动2+1+消费增值:用户留存新策略“

大家好&#xff0c;我是吴军&#xff0c;目前在一家以创新为核心的软件开发公司担任产品经理。今天&#xff0c;我将深入探讨一个经受住了时间考验且依然充满活力的商业模式——“链动21”模式&#xff0c;并通过一个实例及相关数据展示它如何巧妙应对用户留存与复购的挑战。 首…

【JS】在 Node.js 和 Electron 中获取设备 UUID 的最佳实践

在现代应用开发中&#xff0c;识别设备的唯一性是一个常见需求。无论是为了授权、数据跟踪还是用户设备管理&#xff0c;获取设备 UUID 都是实现这些目标的关键。在这篇博客中&#xff0c;我们将探讨如何在 Node.js 和 Electron 中获取设备的 UUID&#xff0c;并比较两种主要方…

vllm 部署GLM4模型进行 Zero-Shot 文本分类实验,让大模型给出分类原因,准确率可提高6%

简介 本文记录了使用 vllm 部署 GLM4-9B-Chat 模型进行 Zero-Shot 文本分类的实验过程与结果。通过对 AG_News 数据集的测试&#xff0c;研究发现大模型在直接进行分类时的准确率为 77%。然而&#xff0c;让模型给出分类原因描述&#xff08;reason&#xff09;后&#xff0c;…

HarmonyOS应用六之应用程序进阶二

目录&#xff1a; 一、进度条通知二、闹钟提醒2.1、在module.json5配置文件中开启权限2.2、导入后台代理提醒reminderAgentManager模块&#xff0c;将此模块命名为reminderAgentManager2.3、如果是新增提醒&#xff0c;实现步骤如下&#xff1a; 3、Native C交互4、第三方库的基…

使用IDEA和vecode创建vue项目并启动

一、使用IDEA创建Vue项目 一、打开IDEA下载Vue插件 打开IDEA的设置找到插件并查找到下载Vue.js这个插件 二、用IDEA创建Vue项目 新建项目并选择到Vue生成器 我这是IDEA自带的 创建项目非常迅速 端口号&#xff08;默认&#xff09;&#xff1a;5173 版本是3.x 启动项目…

使用scss生成旋转圆圈

图片 html代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title>…

modbus tcp wireshark抓包

Modbus TCP报文详解与wireshark抓包分析_mbap-CSDN博客 关于wireshark无法分析出modbusTCP报文的事情_wireshark 协议一列怎么没有modbus tcp-CSDN博客 使用Wireshark过滤Modbus功能码 - 技象科技 连接建立以后才能显示Modbus TCP报文 modbus.func_code 未建立连接时&…

澳鹏干货 | 大语言模型的上下文窗口 (Context Windows)

大语言模型&#xff08;LLMs&#xff09;极大地提升了人工智能在理解和生成文本方面的能力。其中一个影响其效用的重要方面是“上下文窗口”&#xff08;Context Windows&#xff09;—— 这个概念直接影响着模型接收和生成语言的有效性。 本期澳鹏干货将深入探讨上下文窗口对…

AI 自学 Lesson1 - Sklearn(开源Python机器学习包)

目录 背景 作为 lesson1 的原因 一、Sklearn 概述 1. Sklearn 算法库 2. 主要组件 3. 核心流程 4. 自带数据集 二、Sklearn 实操&库名称总结 1. 数据导入 2.数据预处理 2.1 数据划分 2.2 数据变换操作 2.3 特征选择 3. 监督学习算法 3.1 监督学习算法-回归 …

手机控车系统是一种高科技的汽车智能控制系统?

手机控车系统概述 系统概述 移动管家手机控车系统集成了汽车安防、智能化控制及专业配置产品&#xff0c;采用了先进的生产检测设备和质控体系&#xff0c;确保产品质量。该系统支持手机远程控车、远程报警、卫星定位、无匙进入、一键启动、自动升窗等全面功能&#xff0c;为用…

Spark:DataFrame介绍及使用

1. DataFrame详解 DataFrame是基于RDD进行封装的结构化数据类型&#xff0c;增加了schema元数据&#xff0c;最终DataFrame类型在计算时&#xff0c;还是转为rdd计算。DataFrame的结构化数据有Row&#xff08;行数据&#xff09;和schema元数据构成。 Row 类型 表示一行数据 …

C++笔记之原子操作

C++笔记之原子操作 code review! 文章目录 C++笔记之原子操作1.初始化2.赋值3.取值4.赋给另一个原子类型5.`exchange`6.`compare_exchange_weak` 和 `compare_exchange_strong`使用场景7.注意事项在 C++ 中,原子类型提供了对共享变量的无锁操作,确保多线程环境下的安全。以下…

AI的风终于吹到到了短剧,也把财富的风吹到了家门口!

近年来&#xff0c;AI技术在短剧领域的创新应用&#xff0c;给整个行业带来了全新的变革。以快手平台为例&#xff0c;一部以**《山海经》为背景的短剧“李行舟”在今年7月13日上线后引发热议。** 这部短剧讲述了少年李行舟在大海中与古代神灵和各种异兽搏斗的故事。与传统影视…

【C++11】lambda表达式

前言&#xff1a; 随着 C11 的发布&#xff0c;C 标准引入了许多新特性&#xff0c;使语言更加现代化&#xff0c;开发者编写的代码也变得更加简洁和易于维护。Lambda 表达式是其中一个重要的特性&#xff0c;它提供了一种方便的方式来定义匿名函数&#xff0c;这在函数式编程范…

并发——笔试面试总结

1. 进程之间通信的途径有哪些&#xff1f;并说一下他们的通信机制原理 进程间通信的途径包括管道、消息队列、共享内存、信号量、套接字等&#xff0c;以下是几种常见的进程间通信方式及原理&#xff1a; (1) 管道(Pipe) 通信机制原理&#xff1a;管道是一种半双工的通信方式&a…

A0001.主机访问虚拟机中的共享文件完事教程

1. 先在虚拟机中创建一个共享文件夹 2. 在虚拟机的windows系统中查看ip地址 3. 检查网络是否连通 4. 访问虚拟机 5. 登录帐号密码

【JavaEE】——Udp翻译器的实现(回显服务器)

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;引入 1&#xff1a;基本概念 二&#xff1a;UDP socket API使用 1&#xff1a;socke…

正点原子讲解SPI学习,驱动编程NOR FLASH实战

配置SPI传输速度时&#xff0c;需要先失能SPI,__HAL_SPI_DISABLE,然后操作SPI_CR1中的波特率设置位&#xff0c;再使能SPI, NM25Q128驱动步骤 myspi.c #include "./BSP/MYSPI/myspi.h"SPI_HandleTypeDef g_spi1_handler; /* SPI句柄 */void spi1_init(void) {g_spi…

使用Hugging Face中的BERT进行标题分类

使用Hugging Face中的BERT进行标题分类 前言相关介绍出处基本原理优点缺点 前提条件实验环境BERT进行标题分类准备数据集读取数据集划分数据集设置相关参数创建自己DataSet对象计算准确率定义预训练模型定义优化器训练模型保存模型测试模型 参考文献 前言 由于本人水平有限&…

动态规划-简单多状态dp问题——面试题17.16.按摩师

多状态问题的核心就是每个位置不止有一个状态&#xff0c;因此需要多个dp表表示不同状态对应位置的值&#xff0c;然后根据题目考虑特定情况写出状态转移方程即可 1.题目解析 题目来源&#xff1a;面试题17.16.按摩师——力扣 测试用例 2.算法原理 1.状态表示 这里与路径问…