🎯从前端到“后端”:一个使用JWT模拟接口鉴权的完整React项目

143 阅读9分钟

一、项目概述

本项目是一个基于 React 的前端应用,实现了 用户登录、权限控制、路由保护 等功能。通过使用 Zustand 状态管理React Router 路由系统、以及 JWT 令牌鉴权机制,构建了一个结构清晰、逻辑完整的用户认证系统。

该项目适用于初学者和中级开发者,能够帮助他们理解现代 React 应用中常见的 状态管理、路由控制、接口通信、权限验证 等核心概念。

效果图

1. Home首页(/) http://localhost:5173

image.png

2. 登录页面(/login) http://localhost:5173/login

image.png

3. 支付页面(/pay) -- 未登录状态

跳转至 /login 登录页面 image.png

4. 登录(正确的用户名和密码)

image.png 登录成功后跳转至首页: image.png

5. 点击支付页(登录成功后) http://localhost:5173/pay

和未登录状态不一样,未登录时点击支付页会直接跳转到登录页面。 image.png

6. 退出登录 logout

点击Layout会退出登录,重定向到首页。

7. 模拟登录失败

image.png 失败弹出提示框: image.png

二、项目结构概览

项目主要包含以下核心模块:

71cc8599b363a0b4b90c7c28e770ca00.png

三、核心技术栈

文件夹技术栈功能
mockMock.js, JWT:JSON Web Token(用于身份验证)模拟后端 API 数据
apiAxios (HTTP请求库)用户相关接口封装
componentsReact, CSS/SCSS公共组件
storeZustand用户状态管理
viewsReact, CSS/SCSS页面组件
App.jsxReact, Router (路由系统)主组件
main.jsxReact, Router根组件

技术栈安装准备

可以一次性安装所有依赖和第三方库:

pnpm install react react-dom react-router-dom zustand axios jwt

开发期间的依赖:

npm i vite-plugin-mock jwt -D

检查 package.jsonimage.png

四、核心流程解析

用户登录流程:

用户点击登录按钮,调用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>&nbsp;&nbsp;
                    <input 
                        type="text" 
                        id='username' 
                        ref={usernameRef} 
                        placeholder='请输入用户名' 
                        required/>
                </div>
                <div>
                    <label htmlFor="password">Password</label>&nbsp;&nbsp;
                    <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 状态。
  • 登录失败:

    • 抛出错误,前端 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>&nbsp;&nbsp;
            <Link to="/pay">Pay</Link>&nbsp;&nbsp;
            {
                isLogin ? (
                <>
                     <span>欢迎:{user.username}</span>&nbsp;&nbsp;
                    <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,并清除userisLogin状态。

// 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.jsjsonwebtoken,我们可以在前端开发中模拟真实登录流程,包括:

  • 用户登录验证
  • 生成 JWT token
  • 携带 token 请求用户信息
  • 模拟 token 失效或错误的场景

七、总结

项目教学价值

模块教学点
登录流程掌握前后端交互、错误处理、状态更新
路由控制理解 React Router 的基本使用和权限控制
状态管理熟悉 Zustand 的基本用法,理解状态共享机制
接口通信学会使用 Axios 请求接口,理解 mock 数据的作用
权限验证掌握如何使用高阶组件保护路由
用户体验学习如何提示用户错误、控制跳转逻辑

这个项目虽然简单,但涵盖了 React 开发中常见的核心功能:状态管理、路由控制、接口通信、权限验证。它是一个非常适合初学者学习的完整项目,也是中级开发者巩固基础、理解现代前端架构的好例子。

如果你正在学习 React,这个项目将帮助你从零开始构建一个结构清晰、逻辑严谨的前端应用。