一、项目概述
本项目是一个基于 React 的前端应用,实现了 用户登录、权限控制、路由保护 等功能。通过使用 Zustand 状态管理、React Router 路由系统、以及 JWT 令牌鉴权机制,构建了一个结构清晰、逻辑完整的用户认证系统。
该项目适用于初学者和中级开发者,能够帮助他们理解现代 React 应用中常见的 状态管理、路由控制、接口通信、权限验证 等核心概念。
效果图
1. Home首页(/) http://localhost:5173
2. 登录页面(/login) http://localhost:5173/login
3. 支付页面(/pay) -- 未登录状态
跳转至 /login 登录页面
4. 登录(正确的用户名和密码)
登录成功后跳转至首页:
5. 点击支付页(登录成功后) http://localhost:5173/pay
和未登录状态不一样,未登录时点击支付页会直接跳转到登录页面。
6. 退出登录 logout
点击Layout会退出登录,重定向到首页。
7. 模拟登录失败
失败弹出提示框:
二、项目结构概览
项目主要包含以下核心模块:
三、核心技术栈
| 文件夹 | 技术栈 | 功能 |
|---|---|---|
mock | Mock.js, JWT:JSON Web Token(用于身份验证) | 模拟后端 API 数据 |
api | Axios (HTTP请求库) | 用户相关接口封装 |
components | React, CSS/SCSS | 公共组件 |
store | Zustand | 用户状态管理 |
views | React, CSS/SCSS | 页面组件 |
App.jsx | React, Router (路由系统) | 主组件 |
main.jsx | React, Router | 根组件 |
技术栈安装准备
可以一次性安装所有依赖和第三方库:
pnpm install react react-dom react-router-dom zustand axios jwt
开发期间的依赖:
npm i vite-plugin-mock jwt -D
检查 package.json:
四、核心流程解析
用户登录流程:
用户点击登录按钮,调用handleLogin()
↓
调用 useUserStore.login()
↓
调用 doLogin() 发送 POST 请求到 /api/login
↓
服务器验证用户名密码
↓
生成 JWT token 并返回
↓
将 token 存入 localStorage
↓
设置 useUserStore 的 user 和 isLogin = true
↓
NavBar 组件重新渲染,显示欢迎信息
↓
当访问 /pay 页面时:
↓
axios 自动在请求头添加 token
↓
服务器验证 token 后返回用户数据
1. 登录流程(Login → Store → RequireAuth)
完整代码:
view/Login.jsx
import {
useRef
} from 'react'
import {
useNavigate
} from 'react-router-dom'
import {
useUserStore
} from '../../store/user'
const Login = () => {
const usernameRef = useRef();
const passwordRef = useRef();
const {login} = useUserStore(); // useUserStore.login
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
const username = usernameRef.current.value.trim();
const password = passwordRef.current.value.trim();
if(!username || !password){
alert('请输入用户名和密码');
return;
}
try {
// 调用登录方法 useUserStore.login()
await login({ username, password });
// 只有登录成功才会执行到这一步
setTimeout(()=>{
navigate('/')
},1000) // 登录成功跳转至首页
} catch (error) {
// 捕获错误,提示用户
const errorMsg = error.message || '登录失败,请检查用户名或密码';
alert(errorMsg);
}
}
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="text"
id='password'
ref={passwordRef}
placeholder='请输入密码'
required/>
<button type='submit'>Login</button>
</div>
</form>
</>
)
}
export default Login;
store/user.js
import {
create
} from 'zustand'
import {
doLogin
} from '../api/user'
// 使用Zustand创建了一个名为useUserStore的全局状态存储器,
// 用于管理用户的登录状态和信息。
export const useUserStore = create((set) => ({
user: null, // 用户信息 async
isLogin: false, // 是否登录
// 异步函数,接收用户名和密码,调用doLogin进行登录,并设置用户信息和登录状态。
login: async({username="", password=""}) => {
try {
const data = await doLogin({ username, password });
console.log('data.data.code:',data.data.code); // 打印登录状态码
if (data.data.code == 1) {
console.log(data.data.msg); // 打印错误信息
throw new Error(data.msg || '登录失败');
}
const { token, data: user } = data.data;
console.log('///',user)
localStorage.setItem('token', token);
set({ user, isLogin: true });
} catch (error) {
throw error; // 抛出错误,让组件能捕获
}
},
logout: () => {
localStorage.removeItem('token');
set({
user: null,
isLogin: false
})
}
}))
api/user.js
import axios from './config'
// getUser: 获取当前用户的信息。
export const getUser = () => {
return axios.get('/user')
}
// doLogin: 执行登录操作,向服务器提交用户名和密码,并返回登录结果
export const doLogin = (data) => {
return axios.post('/login',data)
}
mock/login.js (不在src目录下,mock和src同级)
import jwt from 'jsonwebtoken';
// mock.js 模拟后端接口逻辑
const secret = '!&124coddefgg';
// login 模块
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,
msg: "用户名或密码错误"
}
}
// 生成token 颁发令牌
const token = jwt.sign({
user: {
id: "001",
username: "admin",
}
}, secret, {
expiresIn: 86400 // 过期时间
})
console.log('mock,jwt:'+token);
return {
token,
data: {
id: "001",
username: "admin"
}
}
}
} ,
{
url: '/api/user',
method: 'get',
response: (req, res) => {
// 用户端 token headers
const token = req.headers["authorization"].split(' ')[1];
console.log(token)
try {
const decode = jwt.decode(token,secret)
console.log(decode)
return {
code: 0,
data: decode.user
}
} catch(error) {
return {
code: 1,
msg: "Invalid token"
}
}
}
}
]
✅ 步骤一:用户输入用户名密码,点击登录
- 用户在
Login.jsx页面输入用户名和密码。 - 使用
useRef获取输入值,调用useUserStore.login()方法。
✅ 步骤二:调用接口进行登录验证
login方法定义在store/user.js中,调用doLogin()(封装了axios请求)。- 向
/api/login发送 POST 请求,携带用户名和密码。
✅ 步骤三:模拟接口返回结果(mock.js)
- 在
mock.js中,模拟了登录接口/api/login。 - 如果用户名密码正确(
admin/123456),则返回 JWT token 和用户信息。 - 如果错误,返回
code: 1和错误信息msg。
✅ 步骤四:处理登录结果
返回 store/user.js
-
登录成功:
- 将 token 存入
localStorage。 - 更新
useUserStore中的user和isLogin状态。
- 将 token 存入
-
登录失败:
- 抛出错误,前端
Login.jsx中捕获并提示用户。
- 抛出错误,前端
✅ 步骤五:跳转首页或提示错误
- 登录成功 → 跳转
/首页。 - 登录失败 → 弹出提示框,不跳转页面。
2. 路由控制流程(React Router)
✅ 路由配置(App.jsx)
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/pay" element={
<RequireAuth>
<Pay />
</RequireAuth>
} />
</Routes>
定义了三个路由:首页/,登录页/login,支付页 /pay。
/pay是受保护的页面,必须通过RequireAuth组件验证登录状态。
✅ 权限控制组件(RequireAuth.jsx)
import {
useUserStore
} from '../../store/user'
import {
useEffect
} from 'react'
import {
useNavigate,
useLocation
} from 'react-router-dom'
const RequireAuth = ({children}) => {
const {isLogin} = useUserStore();
const navigate = useNavigate(); // 用于导航
const { pathname } = useLocation(); // 获取当前路径
useEffect(()=>{
if(!isLogin) {
alert('请先登录');
navigate('/login', { from: pathname})
}
},[isLogin])
return (
<>
{children}
{/* 如果用户已经登录,则直接渲染受保护的内容(比如 <Pay /> 页面) */}
</>
)
export default RequireAuth;
这里的 prop 解构出的是children,代表pay。
如果用户未登录isLogin == false,弹出提示框:请先登录,并重定向到登录页/login,将当前路径pathname=='/pay'传给navigate的第二个参数from,作为传递用户尝试访问的原始路径。
在登录页面中,登录成功后会检查这个 from 参数:
- 如果存在 from 参数,则在登录成功之后使用 navigate(from) 跳转到用户原本想访问的页面。
- 如果不存在 from 参数,则默认导航到主页等。
如果用户已登录isLogin == true,则显示children组件(<Pay />)。
3. 导航栏状态更新(NavBar.jsx)
在App.jsx中把这个组件放在最上。
import {
Link,
} from 'react-router-dom'
import {
useUserStore
} from '../../store/user'
// 由于 useUserStore 的状态变化,
// 任何订阅了这些状态的组件(如 NavBar)都会自动重新渲染,从而显示不同的导航选项。
const NavBar = () => {
const {isLogin, user ,logout} = useUserStore();
return (
<nav style={{padding:10, borderBottom: '1px solid #ccc'}}>
<Link to="/">Home</Link>
<Link to="/pay">Pay</Link>
{
isLogin ? (
<>
<span>欢迎:{user.username}</span>
<button onClick={logout}>Layout</button>
</>
) : (
<Link to="/login">Login</Link>
)
}
</nav>
)
}
export default NavBar;
- 使用
useUserStore获取用户状态:isLogin(是否已登录)、user(用户信息)、logout(退出登录方法)。 - 根据
isLogin状态显示不同的导航栏内容:已登录则显示--欢迎:用户名(user.username)和退出登录按钮。 未登录则只显示三个Link链接。
退出登录
Layout按钮通过点击事件与useUserStore提供的layout方法绑定。当调用这个方法,从本地存储
localStorage移除token,并清除user,isLogin状态。
// store/user.js
logout: () => {
localStorage.removeItem('token');
set({
user: null,
isLogin: false
})
}
五、状态管理(Zustand)
store/user.js
import {
create
} from 'zustand'
import {
doLogin
} from '../api/user'
// 使用Zustand创建了一个名为useUserStore的全局状态存储器,
// 用于管理用户的登录状态和信息。
export const useUserStore = create((set) => ({
user: null, // 用户信息
isLogin: false, // 是否登录
login: async({username="", password=""}) => {
try {
const data = await doLogin({ username, password });
if (data.data.code == 1) {
console.log(data.data.msg); // 打印错误信息
throw new Error(data.msg || '登录失败');
}
const { token, data: user } = data.data;
console.log('///',user)
localStorage.setItem('token', token);
set({ user, isLogin: true });
} catch (error) {
throw error; // 抛出错误,让组件能捕获
}
},
logout: () => {
localStorage.removeItem('token');
set({
user: null,
isLogin: false
})
}
}))
- 使用 Zustand 创建全局状态。
login()方法负责登录逻辑,更新状态。logout()方法清除状态和本地 token。
六、接口通信(API 模拟)
mock/login.js
import jwt from 'jsonwebtoken';
const secret = '!&124coddefgg';
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,
msg: "用户名或密码错误"
}
}
// 生成token 颁发令牌
// json用户数据
const token = jwt.sign({
user: {
id: "001",
username: "admin",
}
}, secret, {
expiresIn: 86400 // 过期时间
})
console.log('mock,jwt:'+token);
return {
token,
data: {
id: "001",
username: "admin"
}
}
}
} ,
{
url: '/api/user',
method: 'get',
response: (req, res) => {
// 用户端 token headers
const token = req.headers["authorization"].split(' ')[1];
console.log(token)
try {
const decode = jwt.decode(token,secret)
console.log(decode)
return {
code: 0,
data: decode.user
}
} catch(error) {
return {
code: 1,
msg: "Invalid token"
}
}
}
}
]
- 使用
mock.js模拟后端接口。 /api/login接口用于用户登录,返回 JWT token 和用户信息,模拟真实登录流程。
JWT 工作原理
1. 创建 JWT(生成 Token)
- 当用户通过账号、密码等凭证登录成功后,服务器会生成一个 JWT。
- 该 JWT 包含了用户的基本信息(如用户ID、用户名等),这些信息称为 claims(声明) 。
- 服务器使用一个密钥(secret)对这个 token 进行签名,确保其不可篡改。
2. 存储 JWT(客户端保存)
-
客户端(通常是浏览器)收到 JWT 后,可以选择保存在:
localStorage:持久化存储,关闭浏览器后依然存在。sessionStorage:仅在当前会话中有效,关闭标签页后清除。- 或者在某些安全要求较高的场景中,不存储在浏览器,而是每次请求都通过 HTTPS 重新获取(更安全但更复杂)。
3. 使用 JWT(请求认证)
-
每次用户请求需要权限的接口时,客户端会将 JWT 放在 HTTP 请求头中,格式如下:
Authorization: Bearer <your-token-here> -
服务器接收到请求后,会对 JWT 的签名进行验证,并检查是否过期、是否被篡改。
-
如果验证通过,服务器会根据 token 中的信息判断用户身份和权限,决定是否返回数据。
通过使用 mock.js 和 jsonwebtoken,我们可以在前端开发中模拟真实登录流程,包括:
- 用户登录验证
- 生成 JWT token
- 携带 token 请求用户信息
- 模拟 token 失效或错误的场景
七、总结
项目教学价值
| 模块 | 教学点 |
|---|---|
| 登录流程 | 掌握前后端交互、错误处理、状态更新 |
| 路由控制 | 理解 React Router 的基本使用和权限控制 |
| 状态管理 | 熟悉 Zustand 的基本用法,理解状态共享机制 |
| 接口通信 | 学会使用 Axios 请求接口,理解 mock 数据的作用 |
| 权限验证 | 掌握如何使用高阶组件保护路由 |
| 用户体验 | 学习如何提示用户错误、控制跳转逻辑 |
这个项目虽然简单,但涵盖了 React 开发中常见的核心功能:状态管理、路由控制、接口通信、权限验证。它是一个非常适合初学者学习的完整项目,也是中级开发者巩固基础、理解现代前端架构的好例子。
如果你正在学习 React,这个项目将帮助你从零开始构建一个结构清晰、逻辑严谨的前端应用。