Reactreact-router-dom

Tạo chức năng xác thực và phân quyền sử dụng Protected Route trong React Router

By Thái Nguyễn
Picture of the author
Published on
image alt attribute

Xác thực và phân quyền là chức năng khá phổ biến trong các web application. Có rất nhiều phương pháp khác nhau để làm chức năng này, mình sẽ chia sẻ cách mình đã sử dụng cho project gần đây của mình đó là tạo HOC Protected Routes để handle logic authen và customize Modal của React Router.

1. Giới thiệu Protected Route

Protected Route chỉ đơn giản là 1 Higher-Order Component. Nó sẽ xử lý logic liên quan đến xác thực và phân quyền, các Page sẽ được bọc bởi Protected Route sẽ được check login authen trước khi render Page.

Tạo component Protected Route như sau:

// ProtectedRoute.tsx
import { memo, ReactNode } from "react";

import { Navigate } from "react-router-dom";
import { Role, User } from "../types";

type ProtectedRouteProps = {
  children: ReactNode;
  redirectPath?: string;
  roles?: Role[];
};

const ProtectedRoute = ({
  redirectPath,
  children,
  roles = [Role.Guest],
}: ProtectedRouteProps) => {
  const user = useSelector((state) => state.user);

  // mock data
  // const user: User = {
  //   name: "Thai Blog",
  //   age: 18,
  //   role: Role.Collaborator,
  // };
  const isGuest = roles.includes(Role.Guest);
  const isMatchRole = roles.includes(user.role);
  let isAllow = isGuest || isMatchRole;

  if (!isAllow) {
    return <Navigate to={redirectPath ?? "/login"} />;
  }

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

export default memo(ProtectedRoute);

Type User và enum Role:

// types/index.ts
export enum Role {
  Admin = "admin",
  Collaborator = "collaborator",
  Guest = "guest",
}

export type User = {
  name: string;
  age: number;
  role: Role;
};

Trong ví dụ trên có 3 role được định nghĩa đó là: admin, collaborator, và guest. Trong component ProtectedRoute, có prop roles là 1 mảng Role enum - nó là cấu hình các role được cho phép truy cập vào Page. Trong trường hợp không truyền giá trị cho prop roles thì default giá trị mặc định sẽ là mảng 1 phần tử chứa Role.Guest ( Guest nghĩa là Trang này không cần quyền, ai cũng có thể truy cập được ).

Như vậy, nếu không truyền prop roles cho component ProtectedRoute hoặc chỉ truyền mảng chứa Role.Guest thì ProtectedRoute sẽ cho phép hiển thị children component.

Nếu role của User không nằm trong list của prop roles thì sẽ bị điều hướng đến page Login hoặc đến path được truyền thông qua redirectPath prop.

Sử dụng ProtectedRoute component với React Router:

// App.tsx
import React from "react";

import "./App.css";
import { Navigate, useRoutes } from "react-router-dom";
import ProtectedRoute from "./hocs/ProtectedRoute";
import AdminPage from "./pages/Admin";
import { Role } from "./types";
const HomePage = React.lazy(() => import("./pages/Home"));
const LoginPage = React.lazy(() => import("./pages/Login"));
const EmptyLayout = React.lazy(() => import("./layouts/EmptyLayout"));
const ApplicationLayout = React.lazy(
  () => import("./layouts/ApplicationLayout")
);

function App() {
  const routes = useRoutes([
    {
      path: "/",
      index: true,
      element: <Navigate to="/home" />,
    },
    {
      path: "/",
      element: <ApplicationLayout />,
      children: [
        {
          path: "home",
          element: <HomePage />,
        },
        {
          path: "admin",
          element: (
            <ProtectedRoute roles={[Role.Admin]}>
              <AdminPage />
            </ProtectedRoute>
          ),
        },
      ],
    },
    {
      path: "/",
      element: <EmptyLayout />,
      children: [
        {
          path: "login",
          element: <LoginPage />,
        },
      ],
    },
  ]);
  return routes;
}

export default App;

Nếu truyền prop roles={[Role.Admin]} như trên, thì chỉ User có role là admin mới được phép truy cập page, nếu không sẽ bị điều hướng đến màn hình Login.

Như vậy chúng ta đã hoàn thành 1 ví dụ đơn giản sử dụng ProtectedRoute để xác thực và phân quyền cho project Reactjs.


Vấn đề khi thêm Protected Route và cách giải quyết

Tuy nhiên với cách làm trên sẽ có nhược điểm đó là khi Project có số lượng page lớn, việc khai báo thêm ProtectedRoute sẽ rất rối và mất thời gian. Nếu có bạn có 50 page thì cần phải bọc các element tương ứng của page với component ProtectedRoute 50 lần.

Để giải quyết vấn đề trên, chúng ta sẽ tạo ra 1 function renderRoutes giúp render các routes theo điều kiện. Thay vì khai báo trực tiếp các routes trong useRoutes hook thì sẽ làm như sau:

// routeUtil.tsx

import { omit } from "lodash";
import { RouteObject } from "react-router-dom";
import { CustomRouteObject } from "../types";
import ProtectedRoute from "../hocs/ProtectedRoute";

const renderRoutes = (routesCustom: CustomRouteObject[]): RouteObject[] => {
  const result = routesCustom.map((route) => {
    let routeRemovedAccess = omit(route, ["roles"]) as RouteObject;

    // thêm protected route nếu có property roles
    if (route.roles && route.element) {
      routeRemovedAccess.element = (
        <ProtectedRoute roles={route.roles} children={route.element} />
      );
    }

    // tiếp tục nếu có children
    if (routeRemovedAccess.children && routeRemovedAccess.children.length > 0) {
      routeRemovedAccess.children = renderRoutes(routeRemovedAccess.children);
    }

    return routeRemovedAccess;
  });

  return result;
};

export { renderRoutes };

renderRoutes function sẽ trả về list object RouteObject của React Router. Input của function này là list CustomRouteObject - 1 type chúng ta tự định nghĩa. Type CustomRouteObject sẽ dựa trên RouteObject. Trong đoạn logic trên mình có sử dụng function omit của lodash để bỏ thuộc tính roles trong Object kiểu CustomRouteObject.

Dưới đây là định nghĩa type CustomRouteObject:

// types/index.ts
...
export type CustomRouteObject = Omit<RouteObject, "children"> & {
  roles?: Role[];
  children?: CustomRouteObject[];
};

Sau này khi logic trở nên phức tạp hơn, chúng ta có thể định nghĩa thêm các properties để đáp ứng được yêu cầu. Bên cạnh đó, việc implement logic xác thực và phân quyền tập trung và dễ dàng.

Cuối cùng chúng ta quay lại file App.tsx để sử dụng function renderRoutes.

import React from "react";

import "./App.css";
import { Navigate, useRoutes } from "react-router-dom";
import AdminPage from "./pages/Admin";
import { CustomRouteObject, Role } from "./types";
import { renderRoutes } from "./utils/routeUtil";
const HomePage = React.lazy(() => import("./pages/Home"));
const LoginPage = React.lazy(() => import("./pages/Login"));
const EmptyLayout = React.lazy(() => import("./layouts/EmptyLayout"));
const ApplicationLayout = React.lazy(
  () => import("./layouts/ApplicationLayout")
);

function App() {
  const customRoutes: CustomRouteObject[] = [
    {
      path: "/",
      index: true,
      element: <Navigate to="/home" />,
    },
    {
      path: "/",
      element: <ApplicationLayout />,
      children: [
        {
          path: "home",
          roles: [Role.Collaborator],
          element: <HomePage />,
        },
        {
          path: "admin",
          roles: [Role.Admin],
          element: <AdminPage />,
        },
      ],
    },
    {
      path: "/",
      element: <EmptyLayout />,
      children: [
        {
          path: "login",
          element: <LoginPage />,
        },
      ],
    },
  ];

  const renderedRoutes = renderRoutes(customRoutes);

  return useRoutes(renderedRoutes);
}

export default App;

Lúc này, để thêm quyền truy cập cho 1 page theo role, chúng ta chỉ cần khai báo thêm thuộc tính roles và khai báo các role được phép truy cập.


Vậy là chúng ta đã hoàn thành chức năng xác thực và phân quyền sử dụng Protected Route trong React Router. Hãy comment nếu bạn có điều gì thắc mắc hoặc muốn trao đổi thêm về bài viết này nhé 🥰