如何使用React Router 6的私有路由(详细教程)

891 阅读6分钟

React Router中的私有路由(也称为受保护的路由)要求用户被授权访问一个路由(读作:页面)。因此,如果一个用户没有被授权访问一个特定的页面,他们就不能访问它。最常见的例子是React应用程序中的身份验证,用户只有在被授权(在这种情况下意味着被认证)时才能访问受保护的页面。不过,授权超出了认证的范围。例如,一个用户也可以有角色和权限,使用户可以访问应用程序的特定区域。

这是一个React Router教程,教你如何使用React Router 6的私有路由。这个React Router v6教程的代码可以在这里找到。

我们将从一个最小的React项目开始,它使用React Router将用户从一个页面导航到另一个页面。在下面的功能组件中,我们有来自React Router的匹配的Link和Route组件,用于各种路线。此外,我们有一个所谓的Index Route,用Landing组件加载,还有一个所谓的No Match Route,用inline JSX加载。两者都充当了回避路由。

import { Routes, Route, Link } from 'react-router-dom';

 const App = () => {
  return (
    <>
      <h1>React Router</h1>

      <Navigation />

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route path="home" element={<Home />} />
        <Route path="dashboard" element={<Dashboard />} />
        <Route path="analytics" element={<Analytics />} />
        <Route path="admin" element={<Admin />} />
        <Route path="*" element={<p>There's nothing here: 404!</p>} />
      </Routes>
    </>
  );
};

const Navigation = () => (
  <nav>
    <Link to="/landing">Landing</Link>
    <Link to="/home">Home</Link>
    <Link to="/dashboard">Dashboard</Link>
    <Link to="/analytics">Analytics</Link>
    <Link to="/admin">Admin</Link>
  </nav>
);

在下文中,我们希望保护所有的路由(除了Landing路由,因为它是一个公共路由)不被非法访问。每个页面都有一个不同的授权机制。只有主页和仪表板页面有相同的授权要求。

const Landing = () => {
  return <h2>Landing (Public: anyone can access this page)</h2>;
};

const Home = () => {
  return <h2>Home (Protected: authenticated user required)</h2>;
};

const Dashboard = () => {
  return <h2>Dashboard (Protected: authenticated user required)</h2>;
};

const Analytics = () => {
  return (
    <h2>
      Analytics (Protected: authenticated user with permission
      'analyze' required)
    </h2>
  );
};

const Admin = () => {
  return (
    <h2>
      Admin (Protected: authenticated user with role 'admin' required)
    </h2>
  );
};

我们将首先模拟一个用户登录/注销机制。通过使用两个有条件渲染的按钮,我们或者根据用户的认证状态渲染一个登录或注销按钮。基于事件处理程序,我们要么设置一个用户,要么通过使用React的useState Hook将其重置为空。

const App = () => {
  const [user, setUser] = React.useState(null);

  const handleLogin = () => setUser({ id: '1', name: 'robin' });
  const handleLogout = () => setUser(null);

  return (
    <>
      <h1>React Router</h1>

      <Navigation />

      {user ? (
        <button onClick={handleLogout}>Sign Out</button>
      ) : (
        <button onClick={handleLogin}>Sign In</button>
      )}

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route path="home" element={<Home user={user} />} />
        ...
      </Routes>
    </>
  );
};

该用户将作为登录或注销的用户为我们服务。接下来我们要保护我们的第一个路由。因此,我们将首先在Home组件中用React Router实现一个重定向,我们已经把user 作为道具传给了该组件。

import { Routes, Route, Link, Navigate } from 'react-router-dom';

...

const Home = ({ user }) => {
  if (!user) {
    return <Navigate to="/landing" replace />;
  }

  return <h2>Home (Protected: authenticated user required)</h2>;
};

当有一个登录的用户时,主页组件不会碰到if-else条件的块,而是渲染主页组件的实际内容。然而,如果没有登录的用户,主页组件会渲染React Router的Navigate组件,从而将用户重定向到登陆页面。如果用户在主页上,并通过点击按钮退出,用户将经历一个来自受保护页面的重定向。

我们用React Router保护了我们的第一个React组件。然而,这种方法不能扩展,因为我们必须在每个受保护的路由中实现相同的逻辑。此外,重定向逻辑不应该存在于Home组件本身,而是作为一个最佳实践从外部保护它。因此,我们将把这些逻辑提取到一个独立的组件中。

const ProtectedRoute = ({ user, children }) => {
  if (!user) {
    return <Navigate to="/landing" replace />;
  }

  return children;
};

然后我们可以使用这个新的保护路由组件作为首页组件的包装。首页组件本身不需要再知道这个保护机制。

const App = () => {
  ...

  return (
    <>
      ...

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route
          path="home"
          element={
            <ProtectedRoute user={user}>
              <Home />
            </ProtectedRoute>
          }
        />
        ...
      </Routes>
    </>
  );
};

const Home = () => {
  return <h2>Home (Protected: authenticated user required)</h2>;
};

这个新的保护路由组件作为整个授权机制的抽象层,保护某些页面不被非法访问。因为我们把它提取为可重用的组件,可以用来另一个(或多个)组件组成它,我们也可以扩展实现细节。例如,在大多数情况下(这里:用户没有被认证),我们想把用户重定向到一个公共路由(例如:'/landing' )。然而,我们也可以通过使用一个可选的道具来确定重定向的路径。

const ProtectedRoute = ({
  user,
  redirectPath = '/landing',
  children,
}) => {
  if (!user) {
    return <Navigate to={redirectPath} replace />;
  }

  return children;
};

当我们需要处理权限和角色的时候,我们会再来扩展这个组件。现在,我们将把这个组件用于其他需要相同保护级别的路由。例如,仪表盘页面也需要用户登录,所以我们来保护这个路由。

const App = () => {
  ...

  return (
    <>
      ...

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route
          path="home"
          element={
            <ProtectedRoute user={user}>
              <Home />
            </ProtectedRoute>
          }
        />
        <Route
          path="dashboard"
          element={
            <ProtectedRoute user={user}>
              <Dashboard />
            </ProtectedRoute>
          }
        />
        <Route path="analytics" element={<Analytics />} />
        <Route path="admin" element={<Admin />} />
        <Route path="*" element={<p>There's nothing here: 404!</p>} />
      </Routes>
    </>
  );
};

保护具有相同授权级别的两个同级路由的更好方法是使用一个Layout Route,它为两个嵌套路由渲染ProtectedRoute组件。

import {
  Routes,
  Route,
  Link,
  Navigate,
  Outlet,
} from 'react-router-dom';

const ProtectedRoute = ({ user, redirectPath = '/landing' }) => {
  if (!user) {
    return <Navigate to={redirectPath} replace />;
  }

  return <Outlet />;
};

const App = () => {
  ...

  return (
    <>
      ...

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route element={<ProtectedRoute user={user} />}>
          <Route path="home" element={<Home />} />
          <Route path="dashboard" element={<Dashboard />} />
        </Route>
        <Route path="analytics" element={<Analytics />} />
        <Route path="admin" element={<Admin />} />
        <Route path="*" element={<p>There's nothing here: 404!</p>} />
      </Routes>
    </>
  );
};

通过使用React Router的Outlet组件而不是React的children道具,你可以使用ProtectedRoute组件作为Layout组件。然而,当试图像以前一样将ProtectedRoute作为包装组件使用时,你的应用程序会中断。因此,当ProtectedRoute不作为Layout组件使用时,你可以选择性地渲染子代。

const ProtectedRoute = ({
  user,
  redirectPath = '/landing',
  children,
}) => {
  if (!user) {
    return <Navigate to={redirectPath} replace />;
  }

  return children ? children : <Outlet />;
};

这就是对私有路由的基本保护,它涵盖了有一个认证用户的基本情况。然而,在一个更复杂的应用程序中,你也会遇到权限和角色。我们将模拟这两种情况,在数组中给我们的用户一个权限和角色,因为他们可能有多个权限。

const App = () => {
  const [user, setUser] = React.useState(null);

  const handleLogin = () =>
    setUser({
      id: '1',
      name: 'robin',
      permissions: ['analyze'],
      roles: ['admin'],
    });

  const handleLogout = () => setUser(null);

  return (...);
};

到目前为止,ProtectedRoute组件只处理作为授权过程的认证用户。我们需要扩展它来处理权限和角色。因此,我们将使开发者能够传入一个布尔值作为条件,作为渲染受保护组件的更抽象的防护。

const ProtectedRoute = ({
  isAllowed,
  redirectPath = '/landing',
  children,
}) => {
  if (!isAllowed) {
    return <Navigate to={redirectPath} replace />;
  }

  return children ? children : <Outlet />;
};

因为我们之前在ProtectedRoute组件中定义了这个条件,我们现在需要从外部定义这个条件。这适用于我们迄今为止的受保护路由,以及需要用户有特定权限或角色的新受保护路由。

const App = () => {
  ...

  return (
    <>
      ...

      <Routes>
        <Route index element={<Landing />} />
        <Route path="landing" element={<Landing />} />
        <Route element={<ProtectedRoute isAllowed={!!user} />}>
          <Route path="home" element={<Home />} />
          <Route path="dashboard" element={<Dashboard />} />
        </Route>
        <Route
          path="analytics"
          element={
            <ProtectedRoute
              redirectPath="/home"
              isAllowed={
                !!user && user.permissions.includes('analyze')
              }
            >
              <Analytics />
            </ProtectedRoute>
          }
        />
        <Route
          path="admin"
          element={
            <ProtectedRoute
              redirectPath="/home"
              isAllowed={!!user && user.roles.includes('admin')}
            >
              <Admin />
            </ProtectedRoute>
          }
        />
        <Route path="*" element={<p>There's nothing here: 404!</p>} />
      </Routes>
    </>
  );
};

主页和仪表盘页面需要用户在场(读作:已认证),而分析和管理页面则需要用户已认证并拥有某些权限/角色。你可以自己尝试一下,撤销用户的角色或权限。

此外,分析和管理页面的保护路径使用了可选的redirectPath 。如果一个用户不符合权限或角色授权的要求,该用户会被重定向到受保护的主页。如果有一个用户首先没有经过认证,他们会被重定向到登陆页面。

如果你是Higher-Order Components的粉丝,你也可以用HoCs创建一个受保护的路由。无论如何,我希望这个教程能帮助你理解React Router中的私有路由(别名受保护的路由),以及如何使用它们作为需要根据用户的认证状态或他们的角色和权限进行授权的路由的护卫。