之前是期望按照用户角色对路由权限进行控制
// routes.tsx
export const routes = [
{
path: "/403",
element: (
<LayoutPage>
<ForbiddenPage />
</LayoutPage>
),
meta: { public: true },
},
{
path: "*",
element: (
<LayoutPage>
<NotFoundPage />
</LayoutPage>
),
meta: { public: true },
},
{
path: "/",
element: <LayoutPage />,
meta: { roles: ["Admin", "user"] },
children: [
{
path: "dashboard/dashboard-analysis",
element: <BusinessIndex />,
meta: { roles: ["Admin", "user"] },
},
{
path: "dashboard/cmp-rule-report",
element: <Transaction />,
meta: { roles: ["Admin", "user"] },
},
{
path: "device-management/inventory-management",
element: <TerminalInventory />,
meta: { roles: ["Admin"] },
},
{
path: "device-management/terminal-type-management",
element: <TerminalType />,
meta: { roles: ["user"] },
},
{
path: "system-management/system-user",
element: <UserManagement />,
meta: { roles: ["Admin"] },
},
{
path: "system-management/system-role",
element: <RoleManagement />,
meta: { roles: ["Admin"] },
},
],
},
{
path: "/login",
element: <LoginPage />,
meta: { public: true },
},
];
// guard.tsx
export const withRouteGuard = (element: JSX.Element, roles?: string[]) => {
return () => {
const location = useLocation();
const { isAuthenticated, userInfo } = useUserStore();
if (!isAuthenticated) {
// 未登录 || token 过期,重定向到登录页
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (roles && !roles.includes(userInfo.role)) {
// 登录了,但没有权限
return <Navigate to="/403" replace />;
}
// 通过权限校验
return element;
};
};
// index.tsx
type RouteMeta = {
public?: boolean;
roles?: string[];
};
interface AppRoute {
path: string;
element: JSX.Element;
children?: AppRoute[];
meta?: RouteMeta;
}
const Router = () => {
const processedRoutes = (routes as AppRoute[]).map(
({ meta, element, children, ...rest }) => {
return {
...rest,
element: meta?.public
? element
: withRouteGuard(element, meta?.roles)(),
children: children?.map(
({ meta: childMeta, element: childElement, ...childRest }) => ({
...childRest,
element: childMeta?.public
? childElement
: withRouteGuard(childElement, childMeta?.roles)(),
})
),
};
}
);
return useRoutes(processedRoutes);
};
export default Router;
这种方法行得通,但是有局限,即无法实现在前端动态的对可展示的路由及页面进行配置,因此开发一个角色权限配置页,类似树形结构的选择器,之后登录或刷新的时候调用接口获得后端返回的数据类似为
export const mockReturnData: {
token: string;
user: {
id: string;
name: string;
roles: string[];
};
permissions: Permission[];
} = {
token: "xxxxx",
user: {
id: "u001",
name: "Cheng",
roles: ["admin"],
},
permissions: [
{ route: "/dashboard/dashboard-analysis", actions: ["write"] },
{ route: "/dashboard/cmp-rule-report", actions: ["read"] },
{ route: "/device-management/inventory-management", actions: ["read"] },
// {
// route: "/device-management/terminal-type-management",
// actions: ["write"],
// },
{ route: "/system-management/system-user", actions: ["write"] },
{ route: "/system-management/system-role", actions: ["write"] },
],
};
permission内的route代表有权限访问的页面及路由,每个子对象内的actions代表页面级的按钮权限,是“只读”还是“可写”。
这种情况下就不需要通过role来判断权限了,将代码改为
// routes.tsx
export const routes = [
{
path: "/403",
element: <ForbiddenPage />,
meta: { public: true },
},
{
path: "*",
element: <NotFoundPage />,
meta: { public: true },
},
{
path: "/login",
element: <LoginPage />,
meta: { public: true },
},
{
path: "/",
element: <LayoutPage />,
children: [
{
path: "/dashboard/dashboard-analysis", // ✅ 改为绝对路径
element: <BusinessIndex />,
},
{
path: "/dashboard/cmp-rule-report",
element: <Transaction />,
},
{
path: "/device-management/inventory-management",
element: <TerminalInventory />,
},
{
path: "/device-management/terminal-type-management",
element: <TerminalType />,
},
{
path: "/system-management/system-user",
element: <UserManagement />,
},
{
path: "/system-management/system-role",
element: <RoleManagement />,
},
],
},
];
// index.tsx
type RouteMeta = {
public?: boolean;
roles?: string[];
};
interface AppRoute {
path: string;
element: JSX.Element;
children?: AppRoute[];
meta?: RouteMeta;
}
const Router = () => {
const processedRoutes = (routes as AppRoute[]).map(
({ meta, element, children, ...rest }) => {
return {
...rest,
element: meta?.public
? element
: withRouteGuard(element, meta?.roles)(),
children: children?.map(
({ meta: childMeta, element: childElement, ...childRest }) => ({
...childRest,
element: childMeta?.public
? childElement
: withRouteGuard(childElement, childMeta?.roles)(),
})
),
};
}
);
return useRoutes(processedRoutes);
};
export default Router;
但这种方法会有副作用,即React Router 在加载父路由时,会预先执行所有子路由的 element
函数,即使你没点进去也会加载(用于匹配路径),所以即使你没点 /device-management/terminal-type-management
,它的 withRouteGuard()
也执行了!
所以要延迟执行权限判断,我们要让 withRouteGuard()
不立即执行,而是在真正匹配路由时才判断权限,所以把 withRouteGuard()
改造成一个组件而不是函数,withRouteGuard(childElement, childMeta?.roles)()这个立即执行了。
最终改为
// guard.tsx
interface RouteGuardProps {
element: JSX.Element;
path: string;
meta?: { public?: boolean };
}
export const RouteGuard = ({ element, path, meta }: RouteGuardProps) => {
const location = useLocation();
const { isAuthenticated } = useUserStore();
const { permissions } = useAuthStore();
if (meta?.public) {
// 公开路由直接放行
return element;
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
const allowedRoutes = permissions.map((p) => p.route);
if (!allowedRoutes.includes(path)) {
return <Navigate to="/403" replace />;
}
return element;
};
// routes.tsx
export const routes = [
{
path: "/403",
element: (
<LayoutPage>
<ForbiddenPage />
</LayoutPage>
),
meta: { public: true },
},
{
path: "*",
element: (
<LayoutPage>
<NotFoundPage />
</LayoutPage>
),
meta: { public: true },
},
{
path: "/",
element: <LayoutPage />,
meta: { public: true }, // <== 根布局路由不校验权限
children: [
{
path: "/dashboard/dashboard-analysis",
element: <BusinessIndex />,
},
{
path: "/dashboard/cmp-rule-report",
element: <Transaction />,
},
{
path: "/device-management/inventory-management",
element: <TerminalInventory />,
},
{
path: "/device-management/terminal-type-management",
element: <TerminalType />,
},
{
path: "/system-management/system-user",
element: <UserManagement />,
},
{
path: "/system-management/system-role",
element: <RoleManagement />,
},
],
},
{
path: "/login",
element: <LoginPage />,
meta: { public: true },
},
];
// index.tsx
type RouteMeta = {
public?: boolean;
roles?: string[];
};
export interface AppRoute {
path: string;
element: JSX.Element;
children?: AppRoute[];
meta?: RouteMeta;
}
const Router = () => {
const wrapRoutes = (routes: AppRoute[]): AppRoute[] => {
return routes.map(({ path, element, meta, children, ...rest }) => {
const isPublic = meta?.public;
return {
...rest,
path,
element: isPublic ? (
element
) : (
<RouteGuard element={element} path={path} meta={meta} />
),
children: children ? wrapRoutes(children) : undefined,
};
});
};
const processedRoutes = wrapRoutes(routes);
return useRoutes(processedRoutes);
};
export default Router;