使用 react-router 在 React 中进行 JWT 身份验证

2025-05-26

使用 react-router 在 React 中进行 JWT 身份验证

在本文中,我们将探讨 JWT 身份验证与 React 和 react-router 的无缝集成。我们还将学习如何处理公共路由、保护已认证路由,以及如何利用 axios 库通过身份验证令牌发出 API 请求。

创建 React 项目

以下命令将为我们创建一个 React 项目。

npm create vite@latest react-auth-demo
Enter fullscreen mode Exit fullscreen mode

我们将选择React作为我们的框架和JavaScript变体。
要开始我们的项目,我们需要确保所有依赖项都已正确安装。我们可以通过运行以下命令来实现:

npm install
Enter fullscreen mode Exit fullscreen mode

在我们的项目目录中。安装过程完成后,我们可以使用命令启动我们的 React 项目

npm run dev
Enter fullscreen mode Exit fullscreen mode

让我们采取这些必要的步骤来使我们的 React 项目顺利启动和运行。

安装 React-Router v6 和 Axios 的依赖项

在继续之前,请确保已安装项目所需的依赖项。首先,我们将安装 react-router v6,它将处理 React 应用程序中的路由。此外,我们还将安装 Axios,这是一个用于发出 API 请求的强大库。按照这些步骤,我们将具备实现无缝路由和高效 API 通信所需的工具。让我们从安装这些依赖项开始。

npm install react-router-dom axios
Enter fullscreen mode Exit fullscreen mode

在 React 中创建 AuthProvider 和 AuthContext
项目设置完毕并安装依赖项后,我们就可以开始实施 JWT 身份验证的下一步了。在本节中,我们将创建一个AuthProvider组件和一个关联的AuthContext。这将使我们能够在整个应用程序中存储和共享与身份验证相关的数据和功能。

在下面的代码片段中,我们将创建authProvider.js位于 的文件。让我们来探索src > provider > authProvider.js的实现AuthProviderAuthContext

  1. 导入必要的模块和包:

    • axios 从“axios”包导入,用于处理 API 请求。
    • createContext、useContext、useEffect、useMemo 和 useState 都是从“react”库导入的。
    import axios from "axios";
    import {
      createContext,
      useContext,
      useEffect,
      useMemo,
      useState,
    } from "react";
    
  2. 使用 createContext() 创建身份验证上下文:

    • createContext() 创建一个空的上下文对象,用于在组件之间共享身份验证状态和功能。
    const AuthContext = createContext();
    
  3. 创建 AuthProvider 组件:

    • 该组件充当身份验证上下文的提供者。
    • 它接收 children 作为 prop,代表可以访问身份验证上下文的子组件。
    const AuthProvider = ({ children }) => {
      // Component content goes here
    };
    
  4. token使用以下方式定义状态useState()

    • token表示身份验证令牌。
    • localStorage.getItem("token")如果存在,则从本地存储中检索令牌值。
    const [token, setToken_] = useState(localStorage.getItem("token"));
    
  5. 创建setToken更新身份验证令牌的函数:

    • 该函数用于设置新的token值。
    • token使用 更新状态setToken_()并使用 将令牌值存储在本地存储中localStorage.setItem()
    const setToken = (newToken) => {
      setToken_(newToken);
    };
    
  6. 用于useEffect()在 axios 中设置默认授权标头,并使用以下方式将令牌值存储在本地存储中localStorage.setItem()

    • 只要值发生变化,此效果就会运行token
    • 如果token存在,它会在 axios 和 localStorage 中设置授权标头。
    • 如果token为空或未定义,则会从 axios 和 localStorage 中删除授权标头。
    useEffect(() => {
      if (token) {
        axios.defaults.headers.common["Authorization"] = "Bearer " + token;
        localStorage.setItem('token',token);
      } else {
        delete axios.defaults.headers.common["Authorization"];
        localStorage.removeItem('token')
      }
    }, [token]);
    
  7. 使用 useMemo() 创建记忆上下文值:

    • 上下文值包括令牌和setToken函数。
    • 标记值用作记忆的依赖项。
    const contextValue = useMemo(
      () => ({
        token,
        setToken,
      }),
      [token]
    );
    
  8. 向子组件提供身份验证上下文:

    • 使用 AuthContext.Provider 包装子组件。
    • 将 contextValue 作为提供者的值属性传递。
    return (
      <AuthContext.Provider value={contextValue}>
        {children}
      </AuthContext.Provider>
    );
    
  9. 导出 useAuth 钩子以访问身份验证上下文:

    • useAuth 是一个自定义钩子,可以在组件中用来访问身份验证上下文。
    export const useAuth = () => {
      return useContext(AuthContext);
    };
    
  10. 将 AuthProvider 组件导出为默认导出:

    • 这允许其他文件根据需要导入和使用 AuthProvider 组件。
    export default AuthProvider;
    

完整代码:

import axios from "axios";
import { createContext, useContext, useEffect, useMemo, useState } from "react";

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  // State to hold the authentication token
  const [token, setToken_] = useState(localStorage.getItem("token"));

  // Function to set the authentication token
  const setToken = (newToken) => {
    setToken_(newToken);
  };

  useEffect(() => {
    if (token) {
      axios.defaults.headers.common["Authorization"] = "Bearer " + token;
      localStorage.setItem('token',token);
    } else {
      delete axios.defaults.headers.common["Authorization"];
      localStorage.removeItem('token')
    }
  }, [token]);

  // Memoized value of the authentication context
  const contextValue = useMemo(
    () => ({
      token,
      setToken,
    }),
    [token]
  );

  // Provide the authentication context to the children components
  return (
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
};

export const useAuth = () => {
  return useContext(AuthContext);
};

export default AuthProvider;
Enter fullscreen mode Exit fullscreen mode

总而言之,这段代码使用 React 的 context API 设置了身份验证上下文。它通过 context 向子组件提供身份验证令牌和 setToken 函数。它还确保每当身份验证令牌发生变化时,axios 中的默认授权标头都会随之更新。

在 React 中创建用于 JWT 身份验证的路由

为了有效地组织路由,我们将创建一个专用src > routes文件夹。在此文件夹中,我们将创建一个index.jsx文件,它将作为定义应用程序路由的入口点。通过在单独的文件夹中构建路由,我们可以维护清晰且易于管理的路由结构。让我们继续创建路由,并探索如何将 JWT 身份验证合并到我们的 React 应用程序中。

为经过身份验证的路由创建 ProtectedRoute 组件

为了保护我们的身份验证路由并防止未经授权的访问,我们将创建一个名为 的专用组件ProtectedRoute。该组件将作为我们身份验证路由的包装器,确保只有经过身份验证的用户才能访问它们。通过实现此组件,我们可以轻松执行身份验证要求并提供无缝的用户体验。让我们创建ProtectedRoute.jsx位于 的文件src > routes > ProtectedRoute.jsx,并增强应用程序的安全性。

  1. 我们首先从 react-router-dom 库导入必要的依赖项:

    import { Navigate, Outlet } from "react-router-dom";
    import { useAuth } from "../provider/authProvider";
    
  2. 我们定义 ProtectedRoute 组件,它将作为我们经过身份验证的路由的包装器:

    export const ProtectedRoute = () => {
      const { token } = useAuth();
    
      // Check if the user is authenticated
      if (!token) {
        // If not authenticated, redirect to the login page
        return <Navigate to="/login" />;
      }
    
      // If authenticated, render the child routes
      return <Outlet />;
    };
    
  3. 在 ProtectedRoute 组件内部,我们从 AuthContext 提供的 useAuth 自定义钩子访问令牌。此钩子允许我们检索存储在上下文中的身份验证令牌。

  4. 然后我们检查 token 是否存在。如果用户未通过身份验证(token 为假或 null),我们使用 react-router-dom 中的 Navigate 组件将用户重定向到登录页面 ("/login")。

  5. 如果用户通过身份验证,我们将使用 Outlet 组件渲染子路由。Outlet 组件充当占位符,显示父路由中定义的子组件。

总而言之,ProtectedRoute 组件充当已认证路由的守卫。如果用户未通过身份验证,则会被重定向到登录页面。如果用户通过了身份验证,则 ProtectedRoute 组件中定义的子路由将使用 Outlet 组件进行渲染。

此代码使我们能够轻松保护特定路由并根据用户的身份验证状态控制访问,从而在我们的 React 应用程序中提供安全的导航体验。

深入探索路由
现在我们已经有了ProtectedRoute组件和身份验证上下文,可以继续定义路由了。通过区分公共路由、已认证路由和未认证用户的路由,我们可以有效地基于 JWT 身份验证处理导航和访问控制。在此代码片段中,我们将深入研究该src > routes > index.jsx文件,并探索如何将 JWT 身份验证整合到我们的路由结构中。让我们开始吧!

  1. 导入必要的依赖项:

    • RouterProvider 和 createBrowserRouter 是从 react-router-dom 库导入的组件。它们用于配置和提供路由功能。
    • useAuth 是从“../provider/authProvider”导入的自定义钩子。它允许我们访问身份验证上下文。
    • ProtectedRoute 是从 "./ProtectedRoute" 导入的组件,用于封装已认证的路由。
    import { RouterProvider, createBrowserRouter } from "react-router-dom";
    import { useAuth } from "../provider/authProvider";
    import { ProtectedRoute } from "./ProtectedRoute";
    
  2. 定义路由组件:

    • 此功能组件充当配置应用程序路由的入口点。
    const Routes = () => {
      const { token } = useAuth();
      // Route configurations go here
    };
    
  3. 使用 useAuth 钩子访问身份验证令牌:

    • 调用 useAuth 钩子从身份验证上下文中检索令牌值。它允许我们在 Routes 组件中访问身份验证令牌。
    const { token } = useAuth();
    
  4. 定义所有用户均可访问的路线:

    • routesForPublic 数组包含所有用户都可以访问的路由对象。每个路由对象由一个路径和一个元素组成。
    • path 属性指定路由的 URL 路径,element 属性包含要渲染的 JSX 元素/组件。
    const routesForPublic = [
      {
        path: "/service",
        element: <div>Service Page</div>,
      },
      {
        path: "/about-us",
        element: <div>About Us</div>,
      },
    ];
    
  5. 定义仅经过身份验证的用户可访问的路由:

    • routesForAuthenticatedOnly 数组包含只有经过身份验证的用户才能访问的路由对象。它包含一个包装在 ProtectedRoute 组件中的受保护根路由(“/”),以及使用 children 属性定义的其他子路由。
    const routesForAuthenticatedOnly = [
      {
        path: "/",
        element: <ProtectedRoute />,
        children: [
          {
            path: "/",
            element: <div>User Home Page</div>,
          },
          {
            path: "/profile",
            element: <div>User Profile</div>,
          },
          {
            path: "/logout",
            element: <div>Logout</div>,
          },
        ],
      },
    ];
    
  6. 定义仅未经身份验证的用户可访问的路由:

    • routesForNotAuthenticatedOnly 数组包含仅对未经身份验证的用户可访问的路由对象。它包含一个登录路由 ("/login")。
    const routesForNotAuthenticatedOnly = [
      {
        path: "/",
        element: <div>Home Page</div>,
      },
      {
        path: "/login",
        element: <div>Login</div>,
      },
    ];
    
  7. 根据身份验证状态组合并有条件地包含路由:

    • createBrowserRouter 函数用于创建路由器配置。它以路由数组作为参数。
    • 扩展运算符(...)用于将路线数组合并为一个数组。
    • 条件表达式 (!token ? routesForNotAuthenticatedOnly : []) 检查用户是否已通过身份验证(令牌是否存在)。如果不存在,则包含 routesForNotAuthenticatedOnly 数组;否则,包含一个空数组。
    const router = createBrowserRouter([
      ...routesForPublic,
      ...(!token ? routesForNotAuthenticatedOnly : []),
      ...routesForAuthenticatedOnly,
    ]);
    
  8. 使用 RouterProvider 提供路由器配置:

    • RouterProvider 组件包装路由器配置,使其可用于整个应用程序。
    return <RouterProvider router={router} />;
    

完整代码:

import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
import { ProtectedRoute } from "./ProtectedRoute";

const Routes = () => {
  const { token } = useAuth();

  // Define public routes accessible to all users
  const routesForPublic = [
    {
      path: "/service",
      element: <div>Service Page</div>,
    },
    {
      path: "/about-us",
      element: <div>About Us</div>,
    },
  ];

  // Define routes accessible only to authenticated users
  const routesForAuthenticatedOnly = [
    {
      path: "/",
      element: <ProtectedRoute />, // Wrap the component in ProtectedRoute
      children: [
        {
          path: "/",
          element: <div>User Home Page</div>,
        },
        {
          path: "/profile",
          element: <div>User Profile</div>,
        },
        {
          path: "/logout",
          element: <div>Logout</div>,
        },
      ],
    },
  ];

  // Define routes accessible only to non-authenticated users
  const routesForNotAuthenticatedOnly = [
    {
      path: "/",
      element: <div>Home Page</div>,
    },
    {
      path: "/login",
      element: <div>Login</div>,
    },
  ];

  // Combine and conditionally include routes based on authentication status
  const router = createBrowserRouter([
    ...routesForPublic,
    ...(!token ? routesForNotAuthenticatedOnly : []),
    ...routesForAuthenticatedOnly,
  ]);

  // Provide the router configuration using RouterProvider
  return <RouterProvider router={router} />;
};

export default Routes;
Enter fullscreen mode Exit fullscreen mode

最终整合

现在我们已经准备好了AuthContextAuthProviderRoutes,让我们将它们集成在一起App.jsx

  1. 导入必要的组件和文件:

    • AuthProvider是从 导入的组件"./provider/authProvider"。它为应用程序提供身份验证上下文。
    • Routes是从 导入的组件"./routes"。它定义了应用程序路由。
    import AuthProvider from "./provider/authProvider";
    import Routes from "./routes";
    
  2. Routes用组件包裹组件AuthProvider

    • AuthProvider组件用于向应用程序提供身份验证上下文。它包装Routes组件,使组件树中的所有组件都可以使用身份验证上下文Routes
    return (
      <AuthProvider>
        <Routes />
      </AuthProvider>
    );
    

完整代码:

import AuthProvider from "./provider/authProvider";
import Routes from "./routes";

function App() {
  return (
    <AuthProvider>
      <Routes />
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

现在一切就绪,是时候实施LoginLogout

让我们为登录页面创建一个文件src > pages > Login.jsx
登录页面

const Login = () => {
  const { setToken } = useAuth();
  const navigate = useNavigate();

  const handleLogin = () => {
    setToken("this is a test token");
    navigate("/", { replace: true });
  };

  setTimeout(() => {
    handleLogin();
  }, 3 * 1000);

  return <>Login Page</>;
};

export default Login;
Enter fullscreen mode Exit fullscreen mode
  • 登录组件是一个代表登录页面的功能组件。
  • 它使用 useAuth 钩子从身份验证上下文导入 setToken 函数。
  • 导入来自 react-router-dom 库的 navigate 函数来处理导航。
  • 在组件内部,有一个 handleLogin 函数,它使用上下文中的 setToken 函数设置测试令牌,并将 replace 选项设置为 true 导航到主页(“/”)。
  • 使用 setTimeout 函数模拟在调用 handleLogin 函数之前延迟 3 秒。
  • 该组件返回登录页面的 JSX,在本例中是一个占位符文本。

现在我们将为注销页面创建一个src > pages > Logout.jsx
文件

import { useNavigate } from "react-router-dom";
import { useAuth } from "../provider/authProvider";

const Logout = () => {
  const { setToken } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    setToken();
    navigate("/", { replace: true });
  };

  setTimeout(() => {
    handleLogout();
  }, 3 * 1000);

  return <>Logout Page</>;
};

export default Logout;
Enter fullscreen mode Exit fullscreen mode
  • 在 Logout 组件中,我们调用setToken没有参数的函数,其等于setToken(null)

现在,我们将用更新的版本替换 Routes 组件中现有的 Login 和 Logout 组件。

const routesForNotAuthenticatedOnly = [
  {
    path: "/",
    element: <div>Home Page</div>,
  },
  {
    path: "/login",
    element: <Login />,
  },
];
Enter fullscreen mode Exit fullscreen mode

数组中routesForNotAuthenticatedOnlyelement的属性"/login"设置为<Login />,表示Login当用户访问“/login”路径时,该组件将被渲染。

const routesForAuthenticatedOnly = [
  {
    path: "/",
    element: <ProtectedRoute />, // Wrap the component in ProtectedRoute
    children: [
      {
        path: "/",
        element: <div>User Home Page</div>,
      },
      {
        path: "/profile",
        element: <div>User Profile</div>,
      },
      {
        path: "/logout",
        element: <Logout />,
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

数组中routesForAuthenticatedOnlyelement的属性"/logout"设置为<Logout />,表示当用户访问“/logout”路径时,会渲染Logout组件。

测试流程

  1. 当您第一次访问根页面时/,您将看到来自数组的“主页” routesForNotAuthenticatedOnly
  2. 如果您导航到/login,经过 3 秒的延迟后,系统将模拟登录过程。它将使用setToken身份验证上下文中的函数设置一个测试令牌,然后/使用库navigate中的函数重定向到根页面react-router-dom。重定向后,您将看到数组中的“用户主页” routesForAuthenticatedOnly
  3. 如果您随后访问/logout,延迟 3 秒后,将模拟注销过程。它将通过调用setToken不带任何参数的函数来清除身份验证令牌,然后您将/再次被重定向到根页面。由于您现在已注销,我们将看到数组中的“主页” routesForNotAuthenticatedOnly

此流程演示了登录和注销过程,其中用户在已认证和未认证状态之间转换,并相应地显示相应的路由。

源代码

文章来源:https://dev.to/sanjayttg/jwt-authentication-in-react-with-react-router-1d03
PREV
使用 HTML、CSS 和 Javascript 构建番茄钟计时器
NEXT
如何使用原子设计来组织你的组件