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

embedded/2024/10/20 19:54:11/

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

1、配置菜单


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

javascript">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、定义用户的全局状态

javascript">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函数里面获取用户信息

javascript">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即可,由于代码量庞大,这里不展开讨论。

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

4、定义权限校验函数

javascript">/*** 检查权限* @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、生成路由

javascript">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中调用路由对象,才能使它生效

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

6、菜单权限校验

javascript">//检查菜单的权限,并做一次深拷贝,得到新的对象
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、按钮权限校验

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

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

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

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


http://www.ppmy.cn/embedded/129072.html

相关文章

SQL第19课——使用存储过程

介绍什么是存储过程&#xff1f;为什么要使用存储过程&#xff1f;如何使用存储过程&#xff1f;创建和使用存储过程的基本语法&#xff1f; 19.1 存储过程 到目前为止&#xff0c;使用的大多数SQL语句都是针对一个或多个表的单条语句。 对于一些复杂的操作需要多条语句才能…

超GPT3.5性能,无限长文本,超强RAG三件套,MiniCPM3-4B模型分享

MiniCPM3-4B是由面壁智能与清华大学自然语言处理实验室合作开发的一款高性能端侧AI模型&#xff0c;它是MiniCPM系列的第三代产品&#xff0c;具有4亿参数量。 MiniCPM3-4B模型在性能上超过了Phi-3.5-mini-Instruct和GPT-3.5-Turbo-0125&#xff0c;并且与多款70亿至90亿参数的…

爬虫逆向学习(十二):一个案例入门补环境

此分享只用于学习用途&#xff0c;不作商业用途&#xff0c;若有冒犯&#xff0c;请联系处理 反爬前置信息 站点&#xff1a;aHR0cDovLzEyMC4yMTEuMTExLjIwNjo4MDkwL3hqendkdC94anp3ZHQvcGFnZXMvaW5mby9wb2xpY3k 接口&#xff1a;/xjzwdt/rest/xmzInfoDeliveryRest/getInfoDe…

线程池原理(一)

一、常用线程池体系结构图如下&#xff1a; 由上边的体系图可以知道&#xff0c;要想了解线程池 ThreadPoolExecutor 的实现原理&#xff0c;则需要先 了解下 Executor、ExecutorService、AbstractExecutorService 的实现&#xff0c;下面就分别看下 这3个类的实现 二、Executo…

6.计算机网络_UDP

UDP的主要特点&#xff1a; 无连接&#xff0c;发送数据之前不需要建立连接。不保证可靠交付。面向报文。应用层给UDP报文后&#xff0c;UDP并不会抽象为一个一个的字节&#xff0c;而是整个报文一起发送。没有拥塞控制。网络拥堵时&#xff0c;发送端并不会降低发送速率。可以…

MongoDB如何查找数据以及条件运算符使用的详细说明

以下是关于MongoDB如何查找数据以及条件运算符使用的详细说明&#xff1a; 查找数据的基本方法 在MongoDB中&#xff0c;使用db.collection.find()方法来查找集合中的数据。如果不添加任何条件&#xff0c;直接使用db.collection.find()会返回集合中的所有文档。例如&#xf…

【STM32 HAL库】MPU6050姿态解算 卡尔曼滤波

【STM32 HAL库】MPU6050姿态解算 卡尔曼滤波 前言MPU6050寄存器代码详解mpu6050.cmpu6050.h 使用说明 前言 本篇文章基于卡尔曼滤波的原理详解与公式推导&#xff0c;来详细的解释下如何使用卡尔曼滤波来解算MPU6050的姿态 参考资料&#xff1a;Github_mpu6050 MPU6050寄存器…

26备战秋招day6——计算机视觉概述

计算机视觉&#xff08;Computer Vision&#xff09;概述 计算机视觉是一个研究如何让机器理解、分析和生成视觉信息的领域。它涉及从图像、视频中获取有意义的信息&#xff0c;目的是通过自动化的方式“看懂”世界。其典型的任务包括&#xff1a;物体识别、图像理解、目标检测…