前端的权限路由到底该咋写? 最近在工作中碰到了一些管理系统的开发需求,其中就包括了整个管理系统从0到1的项目搭建工作。显然,无论你的管理系统面向的是什么类型的客户,或者需要实现什么类型的功能,你都应该在初始项目搭建时,清晰的知道,权限管理功能是最为基础也是最为重要的功能之一。
而今天,就让我们来谈谈,在权限管理中扮演着极为重要角色的一员,权限路由管理。
前言 RBAC 在开始具体的代码实现之前,我们先来好好的回顾一下到底什么是权限管理,以及如何进行权限管理。
通俗的来讲,当不同的用户登录系统的时候,即使是同一个系统,由于身份的不同,他们所能看到的页面
和所能进行的操作
都是不尽相同的,而这些区别则是所谓的权限
,当然,权限本身更多时候是管理者所需要操心的事情。但是,怎么对这些用户以及这些用户的权限进行管理,则是开发者需要操心的事情。
在如今绝大部分的管理系统中,都采用了著名的RBAC权限控制模型来进行权限功能的管理,即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
image.png
很容易可以想到,在用户和权限中间添加了一层角色,大幅度增了权限管理的安全性和效率性。
实现思路 回到正题,权限路由实则就是让服务端来告诉前端,当前这个用户拥有哪些页面的访问权限。换句话来说,当用户访问我们的系统时,他的角色就决定了他能看到哪些页面。
大概的流程为:
image.png
从上述的流程图可以得知,前端在路由管理这块貌似跟RBAC没有任何关系。事实上的确如此,因为RBAC的角色权限模型通常维护在服务端,在用户通过登录态调用接口的时候,服务端就已经根据其用户信息,来得到其对应的角色,最后再告诉前端他的具体权限了,前端自然而然就不用操心这些事情了。
代码实现 让我们进入到具体的代码实现,以React技术栈为例子(Vue基本也大同小异,甚至利用VueRouter的addRoutes会更为简单),前端的路由库基本都是通过React-Router来进行管理。
路由定义 正常在开发的时候,我们会在routes.tsx
中去定义我们全部的页面路由以及访问路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { createBrowserRouter, } from "react-router-dom"; const router = createBrowserRouter([ { path: "/", element: ( <div> <h1>Hello World</h1> </div> ), }, { path: "about", element: <div>About</div>, }, ]); export default router;
然后在页面的根组件中进行引入:
1 2 3 4 5 6 7 8 9 10 import * as React from "react"; import { createRoot } from "react-dom/client"; import { RouterProvider, } from "react-router-dom"; import router from './routes' createRoot(document.getElementById("root")).render( <RouterProvider router={router} /> );
但是,在动态路由的场景下,我们在开发时并不清楚哪些页面会展现,也不清楚页面与页面之间的具体层级结构。换句话来说,所有的页面结构全部由服务端下发。
假设,服务端返回的数据结构为:
1 2 3 4 5 6 7 8 9 10 11 12 const mockRes = [ { // 页面的唯一Key key: 'Home', // 页面url url: '/home', chidrens: [ key: 'User', url: '/user' ] } ]
此时,我们期待用户只能看到/home
和/home/user
两个页面所对应的组件。
在我们重新组织路由相关的代码结构之前,我们先用一个Store来管理需要储存的共享状态(这里使用的是Zustand进行状态管理 https://juejin.cn/post/7274163003157790720 , 当然Redux或者useReducer也都可以 ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 通用状态 * @ loginStatus 是否已登录 * @ permissionRoute 用户路由权限信息 * @ action 状态更新操作 */ interface CommonStoreProps { loginStatus: boolean; permissionRoute: RouteObject[]; action: { updatePermissionRoute: (routes: RouteObject[]) => void; updateLoginStatus: (status: boolean) => void; } } export useRoutesStore = create<RoutesStoreProps>()((set) => ({ loginStatus: false, permissionRoute: [], action: { updateLoginStatus: (status) => set(state => { return { loginStatus: status } }), updatePermissionRoute: (routes) => set(state => ({ permissionRoute: routes})) } })) export const getLoginStatus = () => useCommonStore(state => state.loginStatus); export const getPermissionRoute = () => useCommonStore(state => state.permissionRoute); export const getCommonAction = () => useCommonStore(state => state.action)
然后,我们新增路由的映射文件map.tsx
,在里面根据服务端返回的key
定义好每个页面的映射关系,同时维护好一些不需要权限的公共页面(例如登录页等),以及兜底的404页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React, { lazy } from "react"; /** * 动态路由 */ export const asyncRoutes = { Home: lazy(() => import("@/pages/Home")), User: lazy(() => import("@/pages/User")), }; /** * 公开路由 */ export commonRoutes = { { path: "/login", Component: lazy(() => import("@/pages/login")), }, } /** * 404页面 */ export const notFoundRoutes = { path: "*", Component: lazy(() => import("@/pages/notFound")), };
最后,我们将routes.tsx
改为一个hooks,实现一个工厂函数,在页面初始化的时候默认返回公开路由,当服务端返回权限列表之后,动态转换成React-Router
所需要的路由结构,最终呈现页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { useState, useEffect } from "react"; import { getAction, getPermissionRoute, getLoginStatus } from '@/store' import { commonRoutes, notFoundRoutes } from './map' import { formatPermissionRoutes } from './utils'; import { useRoutes } from "react-router-dom"; const useRouter = () => { // 接口的loading状态 const [loading, setLoading] = useState<boolean>(false) // 更新store权限路由的方法 const { updatePermissionRoute } = getCommonAction() // 订阅当前权限路由,权限路由更新时,重新生成新的全局路由 const permissionRoute = getCommonPermissionRoute() const loginStatus = getCommonLoginStatus() const init = async () => { setLoading(true); const permissionRoutes = await fetchRoutes(); // 利用递归的工厂函数(后续会说),将服务端的路由结构转为`React-Router`的结构 const finallRoutes = formatPermissionRoutes(permissionRoutes); // 更新store里的路由信息,方便其他组件使。,注意,需要接口返回才能把404的兜底路由更新进去,否则一开始就会显示404页面 updatePermissionRoute([ ...routes, notFoundRoutes ]); } /** * 根据默认路由和动态路由,生成路由组件 */ const Router = () => useRoutes([...permissionRoute, ...commonRoutes]); /** * 当用户登录态发生改变时,重新调用接口 */ useEffect(() => { if(loginStatus) { initRoutes() } else { // 判断是否处在公开路由下,否则跳转到登录页 if(!withCommonRoute()) { jumoToLogin() } } }, [loginStatus]) }
页面呈现 在大部分的管理系统开发时,主要的页面结构都会以侧边栏和右侧内容区域为基础样式,这里我利用antd的Layout
组件实现了基本的主体框架,利用React-Router的Outlet
组件,我们的路由就会呈现在Content区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React, { FC } from "react"; import { Outlet } from "react-router-dom"; import { Layout } from "antd"; const { Content } = Layout; const BasicLayout: FC = () => { return ( <Layout> {/* 侧边栏 */} <Sider/> {/* 主要内容 */} <Content> <Outlet></Outlet> </Content> </Layout> ) }
应该怎么组织我们的工厂函数呢?其实很简单,一个递归就搞定了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { asyncRoutes, commonRoutes } from "../routes"; import BasicLayout from "@/layout"; export const formatPermissionRoutes = (routes: ResponseRoutesType[]): RouteObject[] => { if (!routes.length) { return []; } return [{ element: <BasicLayout />, path: "/", children:[ ...recursion(routes) ], }]; }; export const recursion = ( routes: ResponseRoutesType[], formatRoutesList: RouteObject[] = [] ): RouteObject[] => { if (!routes || !routes.length) { return []; } routes.forEach((item) => { formatRoutesList.push({ path: item.url, Component: asyncRoutes[item.key], children: recursionRoutes(item.chidrens), }); }); return formatRoutesList; };
最后,只需要利用我们的Hooks,在接口返回后展现我们的入口组件即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Suspense } from 'react' import { BrowserRouter } from 'react-router-dom' import useRoutes from './routes' function App(){ const { loading, Router } = useRoutes() return ( <Suspense> {loading ? ( loading... ) : ( <BrowserRouter> <Router /> </BrowserRouter> )} </Suspense> ) }
大功告成!
总结 通过上述代码,我们利用React和React-Router提供的强大属性,在比较简短的代码里就实现了一套完整的权限路由体系。