🔧 1. 项目背景与需求分析
在现代 Web 开发中,前后端分离架构已成为主流。后端专注于业务逻辑和数据处理,前端则负责用户交互和界面渲染。这种架构下,如何实现安全的身份验证成为关键问题。传统的 Session 验证方式依赖服务器存储,难以适应分布式系统,而 JWT(JSON Web Token) 提供了无状态、自包含的解决方案。
1.1 为什么选择 JWT?
- 无状态性:JWT 不依赖服务器存储 Session,适合微服务和分布式系统。
- 自包含:Token 中包含用户信息,减少数据库查询压力。
- 跨域友好:Token 可通过 HTTP 头传递,天然支持跨域请求。
- 安全性:通过签名机制防篡改,支持多种加密算法(如 HMAC、RSA)。
1.2 项目目标
本项目将实现一个基于 JWT 的登录鉴权系统,涵盖以下功能:
- 用户登录后生成 JWT 并存储。
- 请求时自动携带 Token 进行身份验证。
- 通过路由守卫限制未授权访问。
- 使用 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
Pay
Login
Pay-Login(登录后)
token获取
控制台响应正常
🧪 2. Mock 后端接口设计
在开发阶段,我们常需要模拟后端接口以验证前端逻辑。以下是 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:用户信息(如
id、username)。 - Signature:使用密钥对 Header 和 Payload 签名,防止篡改。
- Header:声明算法和类型(如
- 过期时间:
expiresIn: "1h"表示 Token 1 小时后失效,需重新登录或刷新 Token。
2.3 安全性建议
- 避免敏感信息:Payload 中不应包含敏感数据(如密码、手机号)。
- HTTPS 传输:确保 Token 在 HTTPS 协议下传输,防止中间人窃取。
- 刷新机制:引入
refresh token延长会话,减少短效 Token 的频繁请求。
🛠 3. Axios 拦截器:自动化 Token 管理
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:全局状态管理
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 存入
localStorage或sessionStorage,页面刷新后恢复状态。 - 异步操作:使用
async/await处理登录请求,避免回调地狱。 - 命名规范:遵循
useXXXStore命名规则,统一状态管理入口。
🚧 5. 路由守卫:保护敏感页面
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中检查用户角色(如admin、user),限制特定页面访问。
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. 登录页面:非受控组件的“逆袭”
登录页面负责收集用户输入并触发登录逻辑。以下是关键代码解析。
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 优化建议
- 表单验证:使用
Yup或Formik实现更复杂的校验规则(如密码强度)。 - 国际化支持:多语言切换(如中英文提示)。
- 记住我功能:通过
localStorage记住用户名,提升用户体验。
🧩 7. 核心组件:NavBar 的实现与作用
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>
<Link to="/pay">Pay</Link>
{isLogin ? (
<>
<span>Welcome, {user.username}</span>
<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 与路由守卫的联动
NavBar 与 RequireAuth 路由守卫共同构成完整的权限控制体系:
- 路由守卫:阻止未授权访问
/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. 其他关键代码片段
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 技术亮点
- 懒加载:
lazy和Suspense延迟加载组件,减少初始加载时间。 - 全局布局:
NavBar作为公共组件,统一页面头部。 - 404 页面:
<Route path="*" />捕获未匹配路径,重定向到登录页。
🧠 9. 关键知识点总结
9.1 JWT 的结构
- Header:声明算法和类型(如
HS256)。 - Payload:用户信息(如
id、username)。 - 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生图还带个二维码🧐
通过本项目,你掌握了:
- JWT 的全流程:从后端签发到前端验证,再到登出时的 Token 清除。
- Zustand 的全局状态管理:集中管理用户登录状态和 Token。
- React Router 的路由守卫:保护敏感页面,限制未授权访问。
- 核心组件的设计:如
NavBar的动态 UI 渲染和导航功能。
下一步学习建议
- 深入安全性:研究 OAuth2.0 与 JWT 的结合使用。
- 性能优化:通过
React.memo和懒加载减少首屏加载时间。 - 微前端集成:将鉴权模块封装为共享库,适配复杂项目架构。