我写个登录怎么就那么难?React + JWT 也太绕了吧!
在现代前端应用开发中,用户认证是核心功能之一,而 JWT(JSON Web Token)凭借其无状态、安全性高等特点,成为主流的身份验证方案。本文将详细介绍如何使用 React 作为 UI 库,结合 Zustand 进行状态管理、Axios 处理网络请求、Mock 模拟后端接口,实现一套完整的 JWT 表单登录校验功能。
一、技术栈简介
在开始实现前,先了解一下本次使用的核心技术:
-
Zustand:轻量级的状态管理库,相比 Redux 更简洁,API 设计直观,适合中小型应用。
-
Axios:基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js,支持请求拦截、响应拦截等功能。
-
**Mock **:用于在浏览器中模拟后端 API,避免依赖真实服务器即可完成开发。
-
JWT:一种紧凑的、URL 安全的令牌(token)格式,用于在各方之间传递声明,通常用于身份验证和信息交换。
用户登录后,服务器验证身份并生成一个包含用户信息的 JSON Web Token。Token 通常由三部分组成:Header(头部)、Payload(载荷)、Signature(签名)。服务器每次收到请求后,解析 请求携带的Token 并验证其签名合法性,确认用户身份。
二、项目初始化与依赖安装
首先创建 React 项目并安装所需依赖:
# 初始化React项目,选择react 与js
npm init vite
# 安装路由依赖
npm i react-router-dom
# 安装axios
npm i axios
# 安装jwt 库
npm i jsonwebtoken
# 安装zustand 状态管理库
npm i zustand
# 安装 mock 插件
pnpm i vite-plugin-mock -D 开发阶段
三、实现步骤
使用mock模拟后端
在vite.config.js中配置后端mock
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { viteMockServe } from 'vite-plugin-mock'
// https://vite.dev/config/
export default defineConfig({
// 添加mock插件
plugins: [react(), viteMockServe({
mockPath: 'mock',
localEnabled: true,
})],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
在项目目录下创建mock目录,在该目录下创建login.js文件,文件内容如下。
登录接口的执行流程:首先从请求体中获取 uername 与 passward ,根据用户名来查询数据库中的用户信息,之后进行用户信息校验,验证用户名和密码,正确则生成 JWT 令牌(包含用户 ID、用户名、角色等信息)并返回。
import jwt from "jsonwebtoken";
// 模拟数据库
// 用户表
const users = [
{
userId: 1,
username: "admin",
passward: "123456",
},
];
// 爱好表
const likes = [
{
userId: 1,
hobbyList: [
{ id: 1, name: "阅读" },
{ id: 2, name: "游泳" },
{ id: 3, name: "跑步" },
],
},d
{
userId: 2,
hobbyList: [
{ id: 4, name: "绘画" },
{ id: 5, name: "音乐" },
],
},
];
// 定义加密的密钥 加盐
const secret = "86_486_0105";
// 创建登录mock
export default [
// 登录接口
{
url: "/login",
method: "POST",
response: (req) => {
const { username, password } = req.body;
console.log("登录参数", username, password);
const user = users.find((item) => item.username === username);
if (user && user.passward === password) {
// 使用算法生成token
const token = jwt.sign(user, secret, {
expiresIn: "1h",
});
return {
code: 0,
msg: "登录成功",
// 生成一个随机的token
data: {
user: {
userId: user.userId,
username: user.username,
},
token,
},
};
}
return {
code: 1,
msg: "用户名或密码错误",
};
},
},
{
url: "/getLikeList",
method: "GET",
response: (req) => {
const token = req.headers["authorization"].split(" ")[1];
try {
// 解析token 获取用户对象
const payload = jwt.verify(token, secret);
// 用户是否存在
const user = findUser(payload.userId);
if (!user) throw new Error("token错误");
// 是否过期
if (payload.exp < Date.now() / 1000) throw new Error("token过期");
// 查询用户爱好
const likeList = likes.find((item) => item.userId === payload.userId);
// 成功
return {
code: 0,
msg: "获取成功",
data: likeList,
};
} catch (error) {
return {
code: 1,
msg: "token错误",
};
}
},
},
];
// 查找数据库
const findUser = (userId) => {
return users.find((item) => item.userId === userId);
};
使用Apifox 对mock模拟的后端进行测试
响应结果如下
{
"code": 0,
"msg": "登录成功",
"data": {
"user": {
"userId": 1,
"username": "admin"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd2FyZCI6IjEyMzQ1NiIsImlhdCI6MTc1MzI1OTEzMCwiZXhwIjoxNzUzMjYyNzMwfQ.T_TpJExql8qdUgminhyzGk67W2ytD_l0C-93BJcVvL8"
}
}
前端
-
封装 Axios 请求与 JWT 拦截器
编写 axios 配置和拦截器,这个拦截器的作用是在每个请求的请求头中中添加Authorization: Bearer ,实现令牌的自动携带。
import axios from "axios"; // 配置基地址 // 拦截请求,添加token const instance = axios.create({ baseURL: "http://localhost:5173", timeout: 1000, headers: { "X-Custom-Header": "foobar" }, }); // 拦截请求,添加token config 是请求配置对象 instance.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) { config.headers["authorization"] = `Bearer ${token}`; } return config; }); // 导出实例 export default instance;编写api请求
import instance from "./config"; export const login = async (data) => { // 登录 设置请求体 return await instance.post('/login', data) } export const getLikeList = async () => { const data = await instance.get('/getLikeList') return data } -
使用 Zustand 管理登录状态
用 Zustand 创建存储用户状态(登录状态、用户信息、令牌)的 store
-
状态:包含user(用户信息)、token(JWT 令牌)、isLogin (是否登录)、loading(加载状态)、error(错误信息)。
-
核心方法:login(调用登录 API 并存储令牌)、logout(清除令牌和用户信息)。
-
令牌存储:使用 localStorage 存储 JWT
import { create } from "zustand"; import { login } from "@/api/user"; const tokenKey = "token"; const useUserStore = create((set) => ({ user: null, // {userId,username} isLogin: false, token: "", loading: false, error: null, setError: (error) => { set({ error }); }, login: async (data) => { set({ loading: true }); const res = await login(data); // 结构响应的数据 const { code, msg, data: userData } = res.data; const { token, user } = userData; // 若登录成功则 设置登录成功信息,并存储token到localStorage中 if (code === 0) { set({ user: user, isLogin: true, token: token, loading: false, }); // 存储token localStorage.setItem(tokenKey, token); } else { set({ loading: false, error: msg, }); } }, logout: () => { set({ user: null, isLogin: false, token: "", }); // 清除本地存储 localStorage.removeItem(tokenKey); }, })); export { useUserStore }; -
-
创建登录表单组件以及相关路由
创建路由
import { useEffect, lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import "./App.css"; const Login = lazy(() => import("@/pages/Login")); const Home = lazy(() => import("@/pages/Home")); const Loading = lazy(() => import("@/components/Loading")); function App() { return ( <> <Suspense fallback={<Loading />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> </Routes> </Suspense> </> ); } export default App;Login 登录组件,包含用户名、密码输入框和登录按钮,
import { useState } from "react"; import Style from "./index.module.css"; import { useNavigate } from "react-router-dom"; import { useUserStore } from "@/store/userStore"; export default function Login() { const { login, error, loading, isLogin, setError } = useUserStore(); // 表单数据 const [formData, setFormData] = useState({ username: "", password: "", }); const navigate = useNavigate(); const handleChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e) => { e.preventDefault(); const { username, password } = formData; // 简单验证 if (!username || !password) { setError("请输入用户名和密码"); return; } await login({ username, password }); console.log("---------------"); console.log(isLogin); if (isLogin) { console.log("nav"); navigate("/"); } }; return ( <div className={Style.loginContainer}> <div className={Style.loginBox}> <h1 className={Style.title}>用户登录</h1> <form onSubmit={handleSubmit} className={Style.form}> {error && <div className={Style.error}>{error}</div>} <div className={Style.inputGroup}> <label htmlFor="username" className={Style.label}> 用户名 </label> <input type="text" id="username" name="username" value={formData.username} onChange={handleChange} className={Style.input} placeholder="请输入用户名" disabled={loading} /> </div> <div className={Style.inputGroup}> <label htmlFor="password" className={Style.label}> 密码 </label> <input type="password" id="password" name="password" value={formData.password} onChange={handleChange} className={Style.input} placeholder="请输入密码" disabled={loading} /> </div> <button type="submit" className={Style.btn} disabled={loading}> {loading ? "登录中..." : "登录"} </button> </form> </div> </div> ); }Home组件
import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import Style from './index.module.css'; import Loading from '@/components/Loading'; import NavBar from '@/components/NavBar'; export default function Home() { const [userInfo, setUserInfo] = useState(null); const [hobbyList, setHobbyList] = useState([]); const [loading, setLoading] = useState(true); const navigate = useNavigate(); // 获取用户信息和爱好列表 const fetchUserInfo = async () => { try { const res = await request.get('/getLikeList'); if (res.code === 0 && res.data) { setHobbyList(res.data.hobbyList || []); // 从token中解析用户信息(实际项目中应从后端获取) const token = localStorage.getItem('token'); if (token) { const payload = JSON.parse(atob(token.split('.')[1])); setUserInfo(payload); } } } catch (error) { console.error('获取用户信息失败:', error); // token无效或过期,重定向到登录页 navigate('/login'); } finally { setLoading(false); } }; // 组件挂载时检查登录状态 useEffect(() => { const token = localStorage.getItem('token'); if (!token) { navigate('/login'); return; } fetchUserInfo(); }, [navigate]); if (loading) { return ( <Loading/> ); } return ( <div className={Style.container}> {/* 导航栏 */} <header className={Style.header}> <NavBar/> </header> {/* 主内容区 */} <main className={Style.main}> <h1 className={Style.title}>欢迎回来,{userInfo?.username}!</h1> <section className={Style.card}> <h2 className={Style.sectionTitle}>你的爱好</h2> {hobbyList.length > 0 ? ( <ul className={Style.hobbyList}> {hobbyList.map(hobby => ( <li key={hobby.id} className={Style.hobbyItem}> {hobby.name} </li> ))} </ul> ) : ( <p className={Style.emptyText}>暂无爱好数据</p> )} </section> </main> {/* 页脚 */} <footer className={Style.footer}> <p>© 2025 JWT Demo 项目</p> </footer> </div> ); }