基于 React 的权限实现

388 阅读4分钟

desola-lanre-ologun-zYgV-NGZtlA-unsplash.jpg

由于公司业务领域的变动,被迫由 Angular 转为了 React,(两者之间有关系吗?🤔)。虽说是赶鸭子上架,但其实上手写业务感觉还好,没有特别吃力。这篇笔记就记录一下基于 React 做权限管理。主要涉及到页面权限的鉴别,暂时不涉及按钮级别(其实是准备以按钮级别权限 + 基于资源的权限管理为内容再水一篇笔记)

完整代码:codesandbox.io/s/react-ts-…

路由守卫的设计

说白了路由守卫不就是在路由变更的时候执行一段函数,从而进行对页面的跳转。所以只要梳理清楚不同页面所需权限以及这些权限的优先级,即能完成路由守卫的编写。

坑爹的是之前做 Angular 或者 Vue 项目的时候,框架有现成的接口。例如 Angular 中的 canActivate 和 Vue 中的 beforeEach,但是 React 没有官方接口或者类。所以在看了一些 React 项目源码之后发现需要自己写一个组件充当路由守卫。

AuthRouter 组件的编写

// AuthRouter.tsx 文件

import { FC } from "react";

type IProps = {
    children: JSX.Element;
};

/**
* 路由守卫组件
*/

const AuthRouter: FC<IProps> = ({ children }) => {
    return children;
};

export default AuthRouter;

index.tsx 中引入并包裹入口页面,这里在哪个文件里面用不是重点,重点是要包裹入口页面。

// index.tsx 文件

import AuthRouter from "./router/AuthRouter";
// App 组件是我的入口页面,里面啥都没有,所有页面都将在 <App /> 组件内渲染

<AuthRouter>
    <App />
</AuthRouter>

现在查看效果就会发现 Page 页面被原封不动的渲染出来了。(别杠我的代码风格,杠就是你赢。我直接从 codesandbox 上粘贴出来的)

贴一下路由配置

这里的配置相对比较简单,不涉及动态路由,后面会单独说动态路由的问题。

/**
 * 路由配置
 */

import { Suspense, lazy, LazyExoticComponent, ReactNode } from "react";
import { RouteObject, Navigate, useRoutes } from "react-router-dom";

/**
 * 自定义懒加载组件,添加 fallback
 * @param {LazyExoticComponent} Element 懒加载组件
 * @returns {ReactNode} react 组件
 */

function lazyLoad(Element: LazyExoticComponent<any>): ReactNode {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Element />
    </Suspense>
   );
}

export interface RouteProps extends RouteObject {
  children?: RouteProps[];
  meta?: {
    title?: string;
    auth?: boolean;
    roles?: string[];
  };

}

// 路由配置
export const routes: RouteProps[] = [
  {
    path: "/",
    element: <Navigate to="login" replace />
  },
  {
    path: "/login",
    element: lazyLoad(lazy(() => import("../pages/Login"))),
    meta: {
      title: "登录页",
      auth: false
    }
  },
  {
    path: "/dashboard",
    element: lazyLoad(lazy(() => import("../pages/Dashboard"))),
    meta: {
      title: "数据看板",
      auth: true,
      roles: ["Admin"]
    }
  },
  {
    path: "*",
    element: <Navigate to="/login" />
  }
];

// 这里用了 ReactRouter V6 的 hook
const Router = () => useRoutes(routes);

export default Router;

给路由守卫添加逻辑

我们先梳理一下守卫的逻辑哈。

  1. 判断目标路由是否需要权限
  2. 如果不需要权限,直接放行
  3. 如果需要权限,判断当前用户是否登录
  4. 若未登录,跳转至登录页面(或指定页面)
  5. 已登录,判断用户是否有该页面权限
  6. 若无权限,跳转至指定页面
  7. 若有权限,直接放行

接下来就是把逻辑翻译成代码语言。

判断目标路由是否需要权限

想要进行判断至少要知道两件事情:

  • 目标路由是啥:可以从 react-router-dom 中的 useLocation 获取 pathname
  • 目标路由的 auth 属性是啥:这个可以从路由配置数组中知道

两者都知道了我们只需要写个查找函数,在路由配置数组中查找满足 path === pathname 的一项就可以知道目标路由是否需要权限。查找函数我就不写了,大家的路由配置表结构可能不一,请自力更生。

// AuthRouter.tsx 文件

import { useLocation } from "react-router-dom";

/**
 * 查找目标路由
 * @param {RouteProps} routes 路由配置数组
 * @returns {string} pathname 目标路由 path
 */
const getTargetPath: RouteProps = (routes: RouteProps[], pathname: string) => {
    // do something
};

const AuthRouter: FC<IPtops> = ({ children }) => {
    // ....
    const { pathname } = useLocation();
    const target = getTargetPath(routes, pathname);
    
    // 目标路由无需权限,直接放行
    if (!target?.auth) return children;
    
    // 需要权限,检查是否已登录
    const token = getToken();
    if (!token) return <Navigate to="/login" />;
    
    // 需要权限,检查是否有该权限
    const roles = intersection(user.roles, target.roles) // intersection 函数为两数组交集,请自立更生
    if (!roles || roles.length <= 0) return <Navigate to="/login" />;
    // ....
};

到此为止其实整个路由守卫就算是写完了,剩下的就是根据实际情况进行一些改造。

但是这种方案有一个对我来说不太友好的地方,getTargetPath 的查找方式不够灵活,我们在配置路由的时候可能会有动态路由,例如 /list/1/detail/11,但是查找又是完全匹配查找,这就导致我明明有该路由,但是找不到。所以我就在想有没有啥方法能直接把目标路由返给我,最好是官方就有相关的 API,翻了一下文档,可是让我找到了一个接口能解决这个事情:matchRoutes

改进版

改造的重点其实就是替换原有的 getTargetPath 函数。

// AuthRouter.tsx 文件

const location = useLocation();
const match = matchRoutes(routes, location);
const target = match[match.length - 1].route;

最后

请各位大佬不吝赐教!🙏🙏🙏