React-Router(V6的权限控制实现示例

目录
  • 接口权限
  • 页面权限
    • 1. 菜单权限
    • 2. 路由权限
  • 按钮权限
    • 参考

      在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。 我们可以大致将权限分为3种: 接口权限页面权限按钮权限

      在这当中,前端主要关注点则是页面权限按钮权限,而前端做这些的主要目的则是:

      • 禁止用户访问一些无权限访问的页面
      • 过滤不必要的请求,减少服务器压力

      下面主要是思路的整理,以及一些核心实现

      接口权限

      接口权限一般是用户登录后,后端根据账号密码来认证授权,并颁发token或者session等来保存用户登录状态。

      后续客户端请求一般是在header中携带token,后端通过对token进行鉴权是否合法来控制是否可以访问接口。

      一般后台会通过用户的角色等来做对应的接口权限控制

      而需要我们前端做的是在请求中携带好登录后回传的token,我们以axios为例

      const instance = axios.create(config);
      instance.interceptors.request.use(
        (request: any) => {
          request.headers["access_token"] = localStorage.getItem("access_token");
          return request;
        },
        (err) => {
          Promise.reject(err.response);
        }
      );
      instance.interceptors.response.use(
        (response) => {
          if (response.status !== 200) return Promise.reject(response.data);
          if (response.data.code === 401) {
            //token过期或者错误
            window.location.replace("/login");
          }
          return response.data.data;
        },
        (err) => {
          Promise.reject(err.response);
        }
      );

      页面权限

      首先,我们先完成路由配置

      src/routes/routes.tsx

      export type RoutesType = {
        path: string;
        element: ReactElement;
        children?: RoutesType[];
      };
      const routers: RoutesType[] = [
        {
          path: "/login",
          element: <Login />,
        },
        {
          path: "/",
          element: <Home />,
        },
        {
          path: "/foo",
          element: <Foo />,
          children: [
            {
              path: "/foo/auth-button",
              element: <MyAuthButtonPage />,
            },
          ],
        },
        {
          path: "/protected",
          element: <Protected />,
        },
        {
          path: "/unauthorized",
          element: <UnauthorizedPage />,
        },
        // 配置404,需要放在最后
        {
          path: "/*",
          element: <NotFound />,
        },
      ];

      然后是基于路由配置来生成对应的路由组件

      src/routes/root.tsx

      const Root = () => {
        // 创建一个有子节点的Route
        const CreateHasChildrenRoute = (route: RoutesType) => {
          return (
            <Route path={route.path} key={route.path}>
              <Route
                index
                element={
                  <AuthRoute key={route.path} path={route.path}>
                    {route.element}
                  </AuthRoute>
                }
              />
              {route?.children && RouteAuthFun(route.children)}
            </Route>
          );
        };
        // 创建一个没有子节点的Route
        const CreateNoChildrenRoute = (route: RoutesType) => {
          return (
            <Route
              key={route.path}
              path={route.path}
              element={
                <AuthRoute path={route.path} key={route.path}>
                  {route.element}
                </AuthRoute>
              }
            />
          );
        };
        // 处理我们的routers
        const RouteAuthFun = (routeList: any) => {
          return routeList.map((route: RoutesType) => {
            let element: ReactElement | null = null;
            if (route.children && !!route.children.length) {
              element = CreateHasChildrenRoute(route);
            } else {
              element = CreateNoChildrenRoute(route);
            }
            return element;
          });
        };
        return (
          <BrowserRouter>
            <Routes>{RouteAuthFun(routers)}</Routes>
          </BrowserRouter>
        );
      };

      最后是只需要在入口中写入Root组件即可

      ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
        <Provider store={store}>
          <Root />
        </Provider>
      );

      上面只是完成了基本的配置,下面才是权限相关

      路由权限主要分为两个方向:

      1. 菜单权限

      一般来说,后台通过维护userrolemenuuser_rolemenu_role这几张表来做相应的权限设计。

      所以,在登录接口中,一般后台会返回用户对应的角色菜单等信息。我们通过redux-toolkit保存登录数据。大致信息如下(未真正请求接口,只写了初始数据):

      src/pages/login/Login.slice.ts

      interface LoginState {
        username: string;
        role: string;
        menuLists: any[];
      }
      // Define the initial state using that type
      const initialState: LoginState = {
        username: "ryo",
        role: "admin",
        menuLists: [
          {
            id: "1",
            name: "首页",
            icon: "icon-home",
            url: "/",
            parent_id: "0",
          },
          {
            id: "2",
            name: "foo",
            icon: "icon-foo",
            url: "/foo",
            parent_id: "0",
          },
          {
            id: "2-1",
            name: "auth-button",
            icon: "icon-auth-button",
            url: "/foo/auth-button",
            parent_id: "2",
          },
        ],
      };

      这里的role表示当前用户的角色,menuLists为用户可访问的菜单

      然后在首页中生成菜单列表

      const getMenuItem = (menus: any): any => {
        return menus.map((menu: any) => {
          if (menu.children) {
            return (
              <div key={menu.url}>
                <Link to={menu.url}>{menu.name}</Link>
                {getMenuItem(menu.children)}
              </div>
            );
          }
          return (
            <div key={menu.url}>
              <Link to={menu.url}>{menu.name}</Link>
            </div>
          );
        });
      };
      function genMenu(array: any, parentId = "0") {
        const result = [];
        for (const item of array) {
          if (item.parent_id === parentId) {
            const menu = { ...item };
            menu.children = genMenu(array, menu.id);
            result.push(menu);
          }
        }
        return result;
      }
      function Home() {
        const menuLists = useAppSelector((state) => state.login.menuLists);
        const menuTree = genMenu(menuLists);
        return (
          <div>
            <h1>home page</h1>
            {getMenuItem(menuTree)}
          </div>
        );
      }
      export default Home;

      但是,只根据权限列表来动态生成菜单并不能完全实现权限相关的目的。用户还可以通过在地址栏输入url的方式来访问没有在菜单中显示的页面。

      2. 路由权限

      我们可以通过实现一个AuthRoute来解决上述的问题。

      通过AuthRoute来拦截页面的访问操作。

      src/routes/AuthRoute.tsx

      // 无需权限认证的白名单
      // 一般是前端的一些报错页
      const DONT_NEED_AUTHORIZED_PAGE = ["/unauthorized", "/*"];
      const AuthRoute = ({ children, path }: any) => {
        // 该flag用于控制 受保护页面的渲染时机,需要等待useEffect中所有的权限验证条件完成后才表示可以渲染
        const [canRender, setRenderFlag] = useState(false);
        const navigate = useNavigate();
        const menuLists = useAppSelector((state) => state.login.menuLists);
        const menuUrls = menuLists.map((menu) => menu.url);
        const token = localStorage.getItem("access_token") || "";
        // 在白名单中的无需验证,直接跳转
        if (DONT_NEED_AUTHORIZED_PAGE.includes(path)) {
          return children;
        }
        useEffect(() => {
          // 用户未登录
          if (token === "") {
            message.error("token 过期,请重新登录!");
            navigate("/login");
          }
          // 已登录
          if (token) {
            // 已登录需要通过logout来控制退出登录或者是token过期返回登录界面
            if (location.pathname == "/login") {
              navigate("/");
            }
            // 已登录,根据后台传的权限列表做判断
            if (!menuUrls.includes(location.pathname)) {
              navigate("/unauthorized", { replace: true });
            }
          }
          // 当上面的权限控制通过后,再渲染受保护的页面
          setRenderFlag(true);
        }, [token, location.pathname]);
        if (!canRender) return null;
        return children;
      };
      export default AuthRoute;

      然后,在我们生成Route的时候在element属性中使用AuthRoute,这一步,我们已经在上面src/routes/root.tsx这个文件中写进去了。

      到这里,我们就通过实现AuthRoute来拦截页面访问,做权限相关处理。

      然后我们可以运行该仓库 代码来看效果。

      目前没有实现登录相关功能,所以需要手动在localStorage中添加access_token来模拟登录。

      • 如果没有登录(没有access_token)或者登录已过期,访问任何路由都会被路由到/login
      • 如果已经登录,但是再访问登录页面,会被路由到/首页
      • 如果已经登录,但是访问了一个你无访问的页面,如/protected,则会被路由到/unauthorized页面

      按钮权限

      按钮级别的权限,根据当前用户角色的不同,可以看到的按钮和操作不同。这里我只简单实现了一个AuthButton

      src/coponents/auth-button/index.tsx

      import { Button } from "antd";
      import type { ButtonProps } from "antd";
      import React from "react";
      import { useAppSelector } from "../../hooks/typedHooks";
      interface AuthButtonProps extends ButtonProps {
        roles: string[];
      }
      const AuthButton: React.FC<AuthButtonProps> = ({ roles, children }) => {
        const role = useAppSelector((state) => state.login.role);
        if (roles.includes(role)) {
          return <Button>{children}</Button>;
        }
        return null;
      };
      export default AuthButton;

      使用方法如下,新增了一个roles属性,表示哪些角色可以看见该按钮

      src/pages/foo/auth-button.tsx

      const ButtonPermission: React.FC = () => {
        const role = useAppSelector((state) => state.login.role);
        return (
          <div>
            <h1>Button Permission</h1>
            <AuthButton roles={["admin", "user"]}>添加</AuthButton>
            <AuthButton roles={["admin"]}>编辑</AuthButton>
            <AuthButton roles={["admin"]}>删除</AuthButton>
          </div>
        );
      };
      export default ButtonPermission;

      我们可以手动的修改Login.slice.ts中的role来查看不同的情况。

      这种实现方式比较简单,大伙可以根据自己的具体场景选择更好的方案

      参考

      • 认证、授权、鉴权和权限控制
      • segmentfault.com/a/1190000020887109

      到此这篇关于React-Router(V6)的权限控制实现示例的文章就介绍到这了,更多相关React-Router权限控制内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

      您可能感兴趣的文章:

      • react 路由权限动态菜单方案配置react-router-auth-plus
      • React-Router如何进行页面权限管理的方法

      Published by

      风君子

      独自遨游何稽首 揭天掀地慰生平

      发表回复

      您的电子邮箱地址不会被公开。 必填项已用 * 标注