🌟 React 中 JWT 登录鉴权实战:小白也能轻松掌握的完整指南(附代码)

237 阅读10分钟

🔧 1. 项目背景与需求分析

image.png

在现代 Web 开发中,前后端分离架构已成为主流。后端专注于业务逻辑和数据处理,前端则负责用户交互和界面渲染。这种架构下,如何实现安全的身份验证成为关键问题。传统的 Session 验证方式依赖服务器存储,难以适应分布式系统,而 JWT(JSON Web Token) 提供了无状态、自包含的解决方案。

1.1 为什么选择 JWT?

  • 无状态性:JWT 不依赖服务器存储 Session,适合微服务和分布式系统。
  • 自包含:Token 中包含用户信息,减少数据库查询压力。
  • 跨域友好:Token 可通过 HTTP 头传递,天然支持跨域请求。
  • 安全性:通过签名机制防篡改,支持多种加密算法(如 HMAC、RSA)。

1.2 项目目标

本项目将实现一个基于 JWT 的登录鉴权系统,涵盖以下功能:

  1. 用户登录后生成 JWT 并存储。
  2. 请求时自动携带 Token 进行身份验证。
  3. 通过路由守卫限制未授权访问。
  4. 使用 Zustand 管理全局用户状态。

1.3 项目目录结构🗂

React/jwt-demo/
├── mock/           # mock 后端接口
│   └── login.js    # 登录与用户信息接口,签发和校验 JWT
├── src/
│   ├── api/        # axios 配置与接口封装
│   │   ├── config.js
│   │   └── user.js
│   ├── components/ # 公共组件
│   │   ├── NavBar.jsx     # 导航栏组件
│   │   └── RequireAuth.jsx# 路由守卫组件
│   ├── store/      # zustand 用户状态管理
│   │   └── user.js
│   ├── views/      # 页面组件
│   │   ├── Home.jsx       # 首页
│   │   ├── Login.jsx      # 登录页
│   │   └── Pay.jsx        # 支付页(受保护页面)
│   ├── App.jsx     # 路由与主逻辑
│   └── main.jsx    # 入口文件
└── README.md

1.4 项目展示

Home image.png

Pay image.png

Login image.png

Pay-Login(登录后) image.png

token获取 image.png

控制台响应正常 image.png


🧪 2. Mock 后端接口设计

image.png

在开发阶段,我们常需要模拟后端接口以验证前端逻辑。以下是 mock/login.js 的详细实现解析。

2.1 JWT 签发与校验

import jwt from "jsonwebtoken";
const secret = "!$123asdfg"; // 加盐,安全第一!

export default [
  {
    url: "/api/login",
    method: "post",
    timeout: 2000,
    response: (req, res) => {
      const { username, password } = req.body;
      if (username !== "admin" || password !== "123456") {
        return { code: 1, message: "用户名或密码错误" };
      }
      const token = jwt.sign(
        { user: { id: "001", username: "admin", level: 4 } },
        secret,
        { expiresIn: "1h" }
      );
      return { token, data: { id: "001", username: "admin", level: 4 } };
    },
  },
  {
    url: "/api/user",
    method: "get",
    response: (req, res) => {
      const authHeader = req.headers["authorization"];
      if (!authHeader) return { code: 1, message: "未携带 token" };
      const token = authHeader.split(" ")[1];
      try {
        const decode = jwt.decode(token, secret);
        return { code: 0, data: decode.user, message: "获取用户信息成功" };
      } catch (err) {
        return { code: 1, message: "Invalid token" };
      }
    },
  },
];

2.2 技术细节解析

  • 加盐(Secret Key)secret 是 JWT 的签名密钥,需保密。若密钥泄露,攻击者可伪造 Token。
  • Token 结构
    • Header:声明算法和类型(如 HS256)。
    • Payload:用户信息(如 idusername)。
    • Signature:使用密钥对 Header 和 Payload 签名,防止篡改。
  • 过期时间expiresIn: "1h" 表示 Token 1 小时后失效,需重新登录或刷新 Token。

2.3 安全性建议

  • 避免敏感信息:Payload 中不应包含敏感数据(如密码、手机号)。
  • HTTPS 传输:确保 Token 在 HTTPS 协议下传输,防止中间人窃取。
  • 刷新机制:引入 refresh token 延长会话,减少短效 Token 的频繁请求。

🛠 3. Axios 拦截器:自动化 Token 管理

image.png

Axios 拦截器是请求和响应的“中间层”,可统一处理 Token 的添加与异常响应。

3.1 请求拦截器

import axios from "axios";

axios.defaults.baseURL = "http://localhost:5173/api";

// 请求拦截器
axios.interceptors.request.use((config) => {
  const token = localStorage.getItem("token") || "";
  if (token) {
    config.headers.Authorization = `Bearer ${token}`; // 自动添加 Token
  }
  return config;
});

export default axios;

3.2 响应拦截器

// 响应拦截器
axios.interceptors.response.use((res) => {
  console.log("响应拦截器触发了!");
  return res;
});

3.3 扩展功能建议

  • 错误处理:捕获 401(未授权)或 403(权限不足)响应,自动跳转登录页。
  • 日志记录:记录请求耗时或异常信息,便于调试。
  • Token 刷新:在拦截器中检测 Token 过期,调用刷新接口。

3.4 user.js

import axios from "./config";

export const getUser = () => {
  return axios.get("/user");
}

export const doLogin = (data) => {
  return axios.post("/login", data);
}

// export const getUserArticles = () => {
//   return axios.get("/user/articles");
// };

🧠 4. Zustand:全局状态管理

image.png

Zustand 是轻量级的状态管理库,适合中小型 React 项目。以下代码展示了用户状态的管理逻辑。

4.1 核心逻辑

import { create } from "zustand";
import { doLogin } from "../api/user";

export const useUserStore = create((set) => ({
  user: null,
  isLogin: false,
  login: async ({ username = "", password = "" }) => {
    const res = await doLogin({ username, password });
    const { token, data: user } = res.data;
    localStorage.setItem("token", token);
    set({ user, isLogin: true }); // 登录成功,更新状态
  },
  logout: () => {
    localStorage.removeItem("token");
    set({ user: null, isLogin: false }); // 登出,清空状态
  },
}));

4.2 与其他状态管理方案对比

方案优点缺点
Zustand简洁易用,无依赖复杂场景需自行管理副作用
Redux社区成熟,工具链完善学习成本高,代码冗余
Context API原生支持,无需额外库多层级组件传递 props 麻烦

4.3 最佳实践

  • 持久化存储:将 Token 存入 localStoragesessionStorage,页面刷新后恢复状态。
  • 异步操作:使用 async/await 处理登录请求,避免回调地狱。
  • 命名规范:遵循 useXXXStore 命名规则,统一状态管理入口。

🚧 5. 路由守卫:保护敏感页面

image.png

RequireAuth 是 React 路由守卫的核心组件,用于保护敏感页面(如 /pay),确保未登录用户无法直接访问。其核心逻辑是通过检查 useUserStore 中的 isLogin 状态,决定是否跳转到登录页面。


🧱 5.1 代码解析

// src/components/RequireAuth.jsx
import { Navigate, useLocation } from "react-router-dom";
import { useUserStore } from "../store/user";

const RequireAuth = ({ children }) => {
  const { isLogin } = useUserStore(); // 获取登录状态
  const location = useLocation(); // 当前路由信息

  if (!isLogin) {
    // 如果未登录,跳转到登录页,并记录当前访问路径
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
};

export default RequireAuth;

🔍 5.2 关键技术点详解

1. 路由守卫的核心逻辑

  • isLogin 检查:通过 Zustand 的 useUserStore 获取用户登录状态。
  • Navigate 组件:未登录时跳转到 /login 页面,并通过 state 传递用户原本想访问的路径(location)。
  • replace 参数:确保用户无法通过浏览器的“返回”按钮绕过登录页。

2. 与路由配置的结合

App.jsx 中,通过包裹 RequireAuth 组件保护敏感路由:

<Route element={<RequireAuth />}>
  <Route path="/pay" element={<Pay />} />
</Route>

🛠 5.3 扩展建议

1. 动态权限控制

  • 角色权限:在 RequireAuth 中检查用户角色(如 adminuser),限制特定页面访问。
const RequireAuth = ({ children, requiredRole }) => {
  const { isLogin, user } = useUserStore();
  const location = useLocation();

  if (!isLogin) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (user.role !== requiredRole) {
    return <Navigate to="/unauthorized" />;
  }

  return children;
};

2. 无感刷新 Token

  • 拦截 401 错误:在 Axios 拦截器中自动刷新 Token,避免用户频繁登录。
// src/api/config.js
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      const newToken = await refreshToken(); // 调用刷新 Token 接口
      localStorage.setItem("token", newToken);
      axios.defaults.headers.common["Authorization"] = `Bearer ${newToken}`;
      return axios(originalRequest);
    }
    return Promise.reject(error);
  }
);

3. 权限提示页

  • 403 页面:当用户权限不足时,跳转到 /unauthorized 页面,提示用户联系管理员。

🧩 5.4 与 NavBar 的联动

  • 敏感页面入口NavBar 中的 /pay 链接仅在用户登录后显示,避免未登录用户尝试访问。
  • 登录后重定向:用户登录成功后,通过 location.state.from 返回到原本想访问的页面。

✅ 5.5 最佳实践总结

特性实现方式 5优势
路由权限控制RequireAuth 组件 + Zustand集中管理权限逻辑,降低组件复杂度
动态跳转Navigate + location.state登录后自动返回原路径,提升用户体验
安全性增强Axios 拦截器自动刷新 Token避免 Token 过期导致的频繁登录

📌 5.6 常见问题解答(FAQ)

Q:如何防止用户通过 URL 直接访问受保护页面?

A:RequireAuth 会在组件加载时检查登录状态,未登录时自动跳转到登录页,无需手动干预。

Q:如何实现基于角色的权限控制?

A:在 RequireAuth 中添加角色检查逻辑,结合后端返回的用户角色信息限制访问。


📝 6. 登录页面:非受控组件的“逆袭”

image.png

登录页面负责收集用户输入并触发登录逻辑。以下是关键代码解析。

6.1 表单处理

import { useRef } from "react";
import { useUserStore } from "../../store/user";
import { useNavigate } from "react-router-dom";

const Login = () => {
  const usernameRef = useRef();
  const passwordRef = useRef();
  const { login } = useUserStore();
  const navigate = useNavigate();
  const handleLogin = (e) => {
    e.preventDefault();
    const username = usernameRef.current.value;
    const password = passwordRef.current.value;
    if (!username || !password) {
      alert("请输入用户名和密码");
      return;
    }
    login({ username, password }); // 调用登录方法
    setTimeout(() => {
      navigate("/"); // 登录成功,跳转首页
    }, 1000);
  };
  return (
    <form onSubmit={handleLogin}>
      <div>
        <label htmlFor="username">Username</label>
        <input
          type="text"
          id="username"
          ref={usernameRef}
          placeholder="请输入用户名"
          required
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          ref={passwordRef}
          placeholder="请输入密码"
          required
        />
      </div>
      <div>
        <button type="submit">登录</button>
      </div>
    </form>
  );
};

export default Login;

6.2 优化建议

  • 表单验证:使用 YupFormik 实现更复杂的校验规则(如密码强度)。
  • 国际化支持:多语言切换(如中英文提示)。
  • 记住我功能:通过 localStorage 记住用户名,提升用户体验。

🧩 7. 核心组件:NavBar 的实现与作用

image.png

NavBar 是用户交互的核心组件,负责:

  • 导航跳转:通过 react-router-dom 的 Link 组件实现页面跳转。
  • 动态展示用户状态:根据 isLogin 显示登录/登出按钮和用户信息。
  • 状态管理集成:通过 useUserStore 获取全局用户状态并触发登出操作。

🧱 7.1 代码解析

import { Link } from "react-router-dom";
import { useUserStore } from "../../store/user";
import { useNavigate } from "react-router-dom";

const NavBar = () => {
  const { isLogin, user, logout } = useUserStore(); // 获取用户状态
  const navigate = useNavigate(); // 导航函数

  return (
    <nav style={{ padding: "10px", borderBottom: "1px solid #ccc" }}>
      <Link to="/">Home</Link>&nbsp;&nbsp;
      <Link to="/pay">Pay</Link>&nbsp;&nbsp;
      {isLogin ? (
        <>
          <span>Welcome, {user.username}</span>&nbsp;&nbsp;
          <button onClick={logout}>Logout</button> {/* 登出按钮 */}
        </>
      ) : (
        <Link to="/login">Login</Link>
      )}
    </nav>
  );
};

export default NavBar;

🔍 7.2 关键技术点详解

1. 状态管理集成

  • useUserStore:从 Zustand 的 user.js 中获取全局状态:

    • isLogin:判断用户是否已登录。
    • user:当前用户信息(如 username)。
    • logout:登出方法,用于清空本地存储和状态。

2. 动态 UI 渲染

  • 条件渲染:通过 isLogin 控制显示内容:

    • 已登录:显示欢迎信息和登出按钮。
    • 未登录:显示登录链接。
  • 组件简洁性:避免冗余逻辑,专注于 UI 展示。

3. 路由跳转

  • Link 组件:实现无刷新跳转,路径对应 App.jsx 中的路由配置。
  • navigate 函数:在登出时可结合 logout 方法跳转页面(当前示例未直接使用,但可扩展)。

🛠 7.3 扩展建议

1. 安全性增强

  • 登出后强制刷新:在 logout 方法中添加 window.location.reload(),确保页面状态同步。
  • 敏感操作确认:为登出按钮添加确认弹窗,防止误操作。

2. UI 优化

  • 样式分离:将 style 提取为 CSS 文件,提升可维护性。
  • 响应式设计:适配移动端,使用 Flex 布局或媒体查询。

3. 多语言支持

  • 国际化:集成 i18next,动态切换语言(如 "Welcome" 和 "登录")。

🧩 7.4 与路由守卫的联动

NavBarRequireAuth 路由守卫共同构成完整的权限控制体系:

  • 路由守卫:阻止未授权访问 /pay 页面。
  • NavBar:在用户登录后显示敏感页面入口(如 /pay),并提供登出入口。

✅ 7.5 最佳实践总结

特性实现方式优势
动态导航react-router-dom + Link无刷新跳转,提升用户体验
状态管理Zustand 全局 Store集中管理用户状态,简化组件逻辑
安全性登出时清除 Token 和状态防止未授权访问残留数据

📌 7.6 常见问题解答(FAQ)

Q:NavBar 如何知道用户是否登录?

A:通过 Zustand 的 useUserStore 获取全局的 isLogin 状态,该状态在登录/登出时自动更新。

Q:为什么登出按钮要调用 logout 方法?

A:logout 方法会清除本地存储的 Token 并重置 Zustand 状态,确保后续请求不再携带失效 Token。

Q:如何扩展 NavBar 的功能(如添加搜索框)?

A:在 nav 内部添加 <input> 元素,结合 useNavigate 实现搜索跳转逻辑。


🧩 8. 其他关键代码片段

image.png

8.1 App.jsx:路由与主逻辑

import { useState, useEffect, lazy, Suspense } from "react";
import { getUser } from "./api/user";
import "./App.css";
import NavBar from "./components/NavBar";
import { Routes, Route, Navigate } from "react-router-dom";

const Home = lazy(() => import("./views/Home"));
const Pay = lazy(() => import("./views/Pay"));
const Login = lazy(() => import("./views/Login"));
const RequireAuth = lazy(() => import("./components/RequireAuth"));

function App() {
  useEffect(() => {
    (async () => {
      const res = await getUser(); // 获取用户信息
      console.log(res, "----res");
    })();
  }, []);

  return (
    <>
      <NavBar />
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/pay" element={
            <RequireAuth>
              <Pay />
            </RequireAuth>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Suspense>
    </>
  );
}

export default App;

8.2 技术亮点

  • 懒加载lazySuspense 延迟加载组件,减少初始加载时间。
  • 全局布局NavBar 作为公共组件,统一页面头部。
  • 404 页面<Route path="*" /> 捕获未匹配路径,重定向到登录页。

🧠 9. 关键知识点总结

image.png

9.1 JWT 的结构

  • Header:声明算法和类型(如 HS256)。
  • Payload:用户信息(如 idusername)。
  • Signature:签名,防止篡改。

9.2 Token 存储位置选择

  • localStorage:简单易用,但易受 XSS 攻击。
  • sessionStorage:生命周期短,刷新页面不丢失。
  • Cookie:可设置 httpOnly,但需防范 CSRF。

9.3 Token 过期与刷新机制

  • 短效 Token:设置较短有效期(如 1 小时),提升安全性。
  • Refresh Token:通过独立接口刷新 Token,避免频繁登录。

9.4 安全注意事项

  • XSS:Token 存储在 localStorage 时,需防范脚本注入。
  • CSRF:Token 放在 httpOnly Cookie 时,需配合 CSRF Token 防护。

🎉 10. 总结:动手实践,成为 JWT 鉴权高手!

AI生图还带个二维码🧐

image.png

通过本项目,你掌握了:

  1. JWT 的全流程:从后端签发到前端验证,再到登出时的 Token 清除。
  2. Zustand 的全局状态管理:集中管理用户登录状态和 Token。
  3. React Router 的路由守卫:保护敏感页面,限制未授权访问。
  4. 核心组件的设计:如 NavBar 的动态 UI 渲染和导航功能。

下一步学习建议

  • 深入安全性:研究 OAuth2.0 与 JWT 的结合使用。
  • 性能优化:通过 React.memo 和懒加载减少首屏加载时间。
  • 微前端集成:将鉴权模块封装为共享库,适配复杂项目架构。