React实现路由鉴权的实例详解

 更新时间:2024年07月01日 09:12:03   作者:sole  
React应用中的路由鉴权是确保用户仅能访问其授权页面的方式,用于已登录或具有访问特定页面所需的权限,这篇文章就来记录下React实现路由鉴权的流程,需要的朋友可以参考下

前言

React应用中的路由鉴权是确保用户仅能访问其授权页面的方式,用于已登录或具有访问特定页面所需的权限,由于React没有实现类似于Vue的路由守卫功能,所以只能由开发者自行实现。前端中的路由鉴权可以区分以下颗粒度:菜单权限控制、组件权限控制、路由权限控制。在后台管理系统中三者是必不可少的。这篇文章就来记录下React实现路由鉴权的流程。

确定权限树

开始之前我们需要对权限树的数据结构进行确定,一般来说我们需要拿到后端传回的权限树进行存储,在React中通常将权限树存储在Redux中,并且我们前端也需要自行维护一颗属于前端的权限树,这里需要两个权限树进行对比从而实现路由鉴权。权限树结构如下:

export type LocalMenuType = {
  key: string;
  level: number; // 层级
  menucode: string; // 权限码 用于查询是否有此权限
  label?: React.ReactNode; // 菜单名称
  path?: string | string[]; // 路由
  parentmenuid?: string; // 父级菜单id
  children?: LocalMenuType[]; // 子菜单
};

权限树处理

在确定权限树后,我们需要对权限树结构进行打平。为此我们可以写个utils来处理权限树。我们使用递归来进行打平权限树:

export const getFlattenList = (
  authList: LocalMenuType[], // 需要打平的权限树
  flattenAuthList: LocalMenuType[], // 存储打平后的权限树
  level?: number // 打平层级
) => {
  authList.forEach((item) => {
    // 如果查找层级超出则返回
    if (level && item.level > level) return;
    flattenAuthList.push(Object.assign({}, item, { children: [] }));
    if (item.children && item.children.length > 0) {
      getFlattenList(item.children, flattenAuthList, level);
    }
  });
};

通过以上代码我们将一个树打平,打平后结构如下:

菜单权限控制

在后台系统中,每个角色有不同的菜单权限,我们需要根据后端返回的角色数据进行菜单的显示与隐藏,为了方便起见这里直接使用mock数据并将数据存储在localStorage中:

export enum RoleType {
  MANAGER,
  ONLY_ORDER,
  ONLY_VIEW_LIST,
}

export const setCurrentRole = (type: RoleType) => {
  switch (type) {
    case RoleType.MANAGER: {
      window.localStorage.setItem("menu", JSON.stringify(menus));
      break;
    }
    case RoleType.ONLY_ORDER: {
      window.localStorage.setItem("menu", JSON.stringify(onlyOrder));
      break;
    }
    case RoleType.ONLY_VIEW_LIST: {
      window.localStorage.setItem("menu", JSON.stringify(onlyViewList));
      break;
    }
  }
};

export const getCurrentRole = () =>
  JSON.parse(window.localStorage.getItem("menu")) as LocalMenuType[];

通过localStorage得到的角色信息进行显示菜单,这里我们只显示1,2级菜单,所以需要对权限菜单进行处理,只保留1,2的菜单数据,也是需要用到递归来处理:

// 处理权限菜单
const handleAuthMenu = (
  menuList: LocalMenuType[],// 全部菜单权限树
  authCodes: string[], // 角色所有权限
  authMenuList: LocalMenuType[], // 角色最终拥有菜单列表
  level?: number // 处理层级
) => {
  menuList.forEach((menu) => {
    // 如果level 存在,则只处理小于level的情况
    if (level && menu.level > level) return;
    // 如果有权限,则继续递归遍历菜单
    if (authCodes.includes(menu.menucode)) {
      let newAuthMenu: LocalMenuType = { ...menu, children: undefined };

      let newAuthMenuChildren: LocalMenuType[] = [];
      if (menu.children && menu.children.length > 0) {
        handleAuthMenu(menu.children, authCodes, newAuthMenuChildren, level);
      }
      // 添加子菜单
      if (newAuthMenuChildren.length > 0) {
        newAuthMenu.children = newAuthMenuChildren;
      }
      authMenuList.push(newAuthMenu);
    }
  });
};
// 获取角色权限菜单
export const getAuthMenu = (flattenAuth: LocalMenuType[], level?: number) => {
  // 获取权限菜单的menucode
  const authCodes: string[] = flattenAuth.map((auth) => auth.menucode);
  let authMenu: LocalMenuType[] = [];

  handleAuthMenu(menus, authCodes, authMenu, level);

  return authMenu;
};

在获取完角色1,2级菜单后,我们需要对左侧菜单栏进行初始化,默认为菜单列表中第一个path。获取二级菜单的首位菜单路由之后通过useNavigate进行跳转。获取菜单路由通过getRoutePath进行获取,由于一个页面可能包含多个路由,所以需要对path信息进行判断:

// anthRoles.ts
// 获取菜单路由
export const getRoutePath = (localMenu: LocalMenuType) => {
  return localMenu.path
    ? typeof localMenu.path === "object"
      ? localMenu.path[0]
      : localMenu.path
    : null;
};
// home.tsx
//2级菜单list
const secondAuthMenuList = useMemo(() => {
    return flattenList.filter((res) => res.level === 2);
  }, [flattenList]);
// 初始化菜单
  useEffect(() => {
    const initMenuItem = secondAuthMenuList[0];
    if (initMenuItem) {
      const initRoute =
        initMenuItem.level > 2 ? pathname : getRoutePath(initMenuItem)!;
      navigate(initRoute, { replace: true });
    }
  }, [secondAuthMenuList, flattenList]);

那如何根据点击的菜单进行跳转呢?也是需要获取对应key值的二级菜单path进行跳转:

const findSecondMenuByKey = (key: string) =>
    secondAuthMenuList.find((item) => item.key === key);
    
    // 点击菜单进行跳转
 const handleMenuChange = ({ key }: { key: string }) => {
    setMenuSelectKeys([key]);
    let chooseItem = findSecondMenuByKey(key);
    if (chooseItem?.path) navigate(getRoutePath(chooseItem) || "");
  };

最终实现效果如下:

组件权限控制

组件权限控制相对简单,需要通过menucode也就是权限码进行组件的显示与隐藏,这里以按钮组件为例子,我们需要一个高阶组件AuthBuutonHOC作为按钮组件的父组件进行显示,同时,我们通过hasAuth函数判断是否有当前指定权限:

// 是否有当前权限
export const hasAuth = (meunCode: string) => {
  // 当前打平的角色权限树
  let flattenAuthList: LocalMenuType[] = getCurrentFlattenRole();
  return !!flattenAuthList.find((auth) => auth.menucode === meunCode);
};
AuthBuutonHOC.tsx

const AuthButton: React.FC<Props> = ({ menuCode, children }) => {
  // 没有当前权限则不显示
  if (!hasAuth(menuCode)) return null;

  return <>{children}</>;
};

export default React.memo(AuthButton);

使用AuthButton包裹按钮,就能实现组件级别的权限控制:

  • 有当前权限:

  • 无当前权限:

路由权限控制

路由权限控制需要对用户输入的路径名进行校验,我们通过useLocation获取到当前用户输入的pathname;并路径匹配matchPath判断当前路径与权限菜单路径是否对应,若对应上则表示当前角色拥有权限,若对应不上则跳转到404页面。

通过以上逻辑,我们先创建一个hasAuthByRoutePath函数来判断是否有当前的路由权限:

// 是否有当前的路由权限
export const hasAuthByRoutePath = (path: string) => {
  let flattenAuthList: LocalMenuType[] = getCurrentFlattenRole();
  return !!flattenAuthList.find((auth) =>
    routePathMatch(path, auth.path || "")
  );
};

// 判断路由是否一致
export const routePathMatch = (path: string, menuPath: string | string[]) => {
  if (typeof menuPath === "object") {
    return menuPath.some((item) => matchPath(item, path));
  }

  return !!matchPath(menuPath, path);
};

在home页面监听用户输入的路径名进行判断,有则跳转到当前菜单:

useEffect(() => {
    // 获取当前匹配到的菜单
    const matchMenuItem = flattenList.find((item) =>
      routePathMatch(pathname, item.path || "")
    );
    if (matchMenuItem) {
      // 如果当前菜单level为3级或者更小则设置其父id,否则设置其id
      matchMenuItem.level > 2
        ? setMenuSelectKeys([matchMenuItem.parentmenuid!])
        : setMenuSelectKeys([matchMenuItem.key]);
      // 如果当前菜单level为3级或者更小则通过父id找到2级菜单
      const newSecondMenu =
        matchMenuItem.level > 2
          ? findSecondMenuByKey(matchMenuItem.parentmenuid!)
          : matchMenuItem;
      // 有对应的二级菜单则定位到当前侧边菜单栏位置
      if (newSecondMenu) {
        setMenuOpenKeys((preOpenKeys) => {
          const openKeysSet = new Set(preOpenKeys || []);
          openKeysSet.add(newSecondMenu.parentmenuid!);
          return Array.from(openKeysSet);
        });
      } else {
        setMenuSelectKeys([]);
      }
    }
  }, [secondAuthMenuList, flattenList, pathname]);

最后,如果没有当前路径权限则跳转到404页面,为此我们需要一个authLayout高阶组件来包裹HomePage来实现跳转:

// 白名单
const routerWhiteList = ["/home"];

const AuthLayout = ({ children }: { children: JSX.Element }) => {
  const { pathname } = useLocation();

  // 判断当前路由是否在白名单内或者有当前权限路由
  const hasAuthRoute = useMemo(() => {
    return (
      routePathMatch(pathname, routerWhiteList) || hasAuthByRoutePath(pathname)
    );
  }, [pathname]);
   // 没有权限则跳转至404页面
  if (!hasAuthRoute) return <Navigate to="/404" replace />;

  return children;
};

export default AuthLayout;

将以上组件包裹在HomePage外层即可实现路由权限控制:

{
    path: "/home",
    element: (
      <AuthLayout>
        <HomePage />
      </AuthLayout>
    ),
}

具体实现效果如下:

总结

到此这篇关于React实现路由鉴权的实例详解的文章就介绍到这了,更多相关React路由鉴权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一起来了解React的Hook

    一起来了解React的Hook

    这篇文章主要为大家详细介绍了React的Hook,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • React利用props的children实现插槽功能

    React利用props的children实现插槽功能

    React中并没有vue中的 slot 插槽概念 不过 可以通过props.children 实现类似功能,本文为大家整理了实现的具体方,需要的可以参考一下
    2023-07-07
  • React项目中使用zustand状态管理的实现

    React项目中使用zustand状态管理的实现

    zustand是一个用于状态管理的小巧而强大的库,本文主要介绍了React项目中使用zustand状态管理的实现,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • 在React中实现Vue的插槽功能的示例代码

    在React中实现Vue的插槽功能的示例代码

    在 Vue 中,插槽(Slots)允许父组件向子组件传递 HTML 结构,从而实现更灵活的组件复用,具名插槽允许父组件向子组件传递多个不同的 HTML 结构,在 React 中,我们没有直接的插槽概念,但可以通过 props.children 和函数作为 props 来实现类似的功能
    2025-01-01
  • 在Create React App中使用CSS Modules的方法示例

    在Create React App中使用CSS Modules的方法示例

    本文介绍了如何在 Create React App 脚手架中使用 CSS Modules 的两种方式。有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
    2019-01-01
  • react实现pure render时bind(this)隐患需注意!

    react实现pure render时bind(this)隐患需注意!

    这篇文章主要为大家详细介绍了值得你在react实现pure render的时候,需要注意的bind(this)隐患,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-03-03
  • 更强大的React 状态管理库Zustand使用详解

    更强大的React 状态管理库Zustand使用详解

    这篇文章主要为大家介绍了更强大的React 状态管理库Zustand使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • React如何实现Vue的watch监听属性

    React如何实现Vue的watch监听属性

    在 Vue 中可以简单地使用 watch 来监听数据的变化,还能获取到改变前的旧值,而在 React 中是没有 watch 的,今天通过本文给大家讲解React实现Vue的watch监听属性的方法,需要的朋友可以参考下
    2023-06-06
  • react项目中redux的调试工具不起作用的解决

    react项目中redux的调试工具不起作用的解决

    这篇文章主要介绍了react项目中redux的调试工具不起作用的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • react的严格模式和解决react useEffect执行两次问题

    react的严格模式和解决react useEffect执行两次问题

    文章总结:本文详细探讨了React中useEffect执行两次的问题,主要归因于React的严格模式,严格模式在开发模式下会故意重复调用一些生命周期方法,以帮助开发者发现潜在的问题,包括不安全的生命周期、过时的ref API、废弃的findDOMNode方法、意外的副作用等
    2025-01-01

最新评论