记录一下后台管理系统的路由权限管理

之前是期望按照用户角色对路由权限进行控制

// 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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值