🔐 前端鉴权不再难!手把手带你搞懂 JWT + Zustand + Axios 的完整登录方案
摘要:还在为前端鉴权头疼?本文通过一个完整的实战 Demo,带你从零理解 JWT 鉴权的完整流程,掌握 Zustand 状态管理 + Axios 拦截器 + 路由守卫的最佳实践!
📌 前言
上周,有个学弟问我:"哥,我们项目要做登录功能,后端说用 JWT,但我不知道前端怎么配合啊?"
我当时一愣,这不就是我刚入行时的困惑吗?
在前端开发中,登录鉴权 是一个绕不开的话题。很多同学对以下概念一头雾水:
- Token 是什么?为什么要用 JWT?
- 前端怎么存储用户状态?
- 如何让未登录用户无法访问某些页面?
- Axios 拦截器到底有什么用?
说实话,我刚学的时候也是一脸懵,看了很多文章都是"纸上谈兵",直到自己动手写了一个 Demo 才真正搞明白。
今天,我把这个 React + Vite + Zustand + JWT 的实战 Demo 分享出来,帮你彻底理清这些概念,搞懂前端鉴权的完整链路!
💡 学习建议:别光看,一定要自己跑一遍 Demo,动手才能真正理解!
🎯 本文适合谁
- ✅ 想理解 JWT 鉴权原理的前端开发者
- ✅ 想学习 Zustand 状态管理的 React 开发者
- ✅ 想掌握 Axios 拦截器用法的同学
- ✅ 需要实现路由守卫的项目开发者
📚 核心概念:从 HTTP 无状态说起
HTTP 是无状态的
HTTP 协议本身是 无状态(Stateless) 的,服务器不知道每次请求的用户是谁:
客户端请求 → 服务器响应 → 完事儿,服务器不认识你了
那怎么让服务器知道 "我是谁" 呢?
传统方案:Cookie + Session
scss
用户登录 → 服务器创建 Session → 返回 Cookie(sessionId)
↓
用户再次请求 → 携带 Cookie → 服务器查找 Session → 认证成功
问题:
- Session 存储在服务器内存,不适合分布式部署
- Cookie 每次请求都会自动携带,有 CSRF 风险
现代方案:JWT (JSON Web Token)
JWT 的核心思想:把用户身份信息加密后直接发给客户端,客户端每次请求时带上这个令牌。
用户登录 → 服务器签发 Token → 客户端存储
↓
用户再次请求 → 携带 Token → 服务器验证并解码 → 认证成功
优势:
- 无状态,服务器不需要存储 Session
- 支持分布式部署,任何服务器都能验证 Token
- 可以携带自定义数据(用户角色、权限等)
🔧 技术方案设计
整体架构
bash
┌─────────────────────────────────────────────────────────┐
│ 前端应用 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Login │ │ Home │ │ Pay │ │ Nav │ │
│ └────┬────┘ └─────────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Zustand Store (user.js) │ │
│ │ 管理 token、user 全局状态 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Axios 实例 (config.js) │ │
│ │ 请求拦截器:自动添加 Authorization header │ │
│ │ 响应拦截器:统一处理返回数据 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
└───────┼─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Mock 服务器 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ /api/login │ │ /api/repo │ │
│ │ 签发 Token │ │ 验证 Token │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
技术栈选择
| 技术 | 作用 | 为什么选它 |
|---|---|---|
| React 19 | UI 框架 | 主流前端框架,生态成熟 |
| Zustand | 状态管理 | 轻量、简单、无 Provider |
| Axios | HTTP 请求 | 支持拦截器、功能强大 |
| vite-plugin-mock | Mock 数据 | 本地开发无需后端 |
| jsonwebtoken | JWT 签发验证 | 模拟后端 JWT 逻辑 |
💭 我为什么选 Zustand 而不是 Redux?
说实话,之前我也用 Redux,但光配置就要写一堆:
javascript
// Redux 的 boilerplate 真的多...
const ActionTypes = { LOGIN: 'LOGIN', LOGOUT: 'LOGOUT' };
const loginAction = (user) => ({ type: ActionTypes.LOGIN, payload: user });
const authReducer = (state, action) => { /* ... */ };
// 还要 createStore、Provider 包裹...
换了 Zustand 后,一个 create 搞定:
javascript
// Zustand 简洁多了!
const useAuthStore = create(set => ({
token: '',
setAuth: (token) => set({ token }),
}));
// 直接用,不需要 Provider!
代码量直接减少 60%,而且学习成本极低,5 分钟上手!
当然,Redux 也有它的优势(中间件、DevTools),但对中小项目来说,Zustand 完全够用,而且更香!
📝 代码实现详解
1️⃣ Mock 层:模拟后端 JWT 签发和验证
mock/user.js 是整个鉴权流程的起点,模拟后端的登录和鉴权接口:
javascript
import jwt from 'jsonwebtoken';
// JWT 密钥(生产环境应该存储在环境变量中)
const secret = 'secret819!$';
export default [
{
// 需要鉴权的接口:验证 Token
url: '/api/repo',
method: 'get',
response: req => {
// 1. 从请求头获取 Authorization
const auth = req.headers['authorization'] || '';
const token = auth.split(' ')[1]; // 提取 Bearer 后面的 token
// 2. 没有 token 返回 401
if (!token) {
return { code: 401, message: 'No token provided' };
}
// 3. 验证 token 是否有效
try {
let decoded = jwt.verify(token, secret);
return { code: 0, data: decoded.user };
} catch (err) {
return { code: 401, message: 'Invalid token' };
}
}
},
{
// 登录接口:签发 Token
url: '/api/login',
method: 'post',
response: (req, res) => {
const body = req.body;
// 1. 验证用户名密码
if (body.username !== 'admin' || body.password !== '123456') {
return { code: -1, message: 'username or password error' };
}
// 2. 签发 JWT Token
const token = jwt.sign(
{ user: body.username, role: 'admin' }, // 用户信息
secret, // 密钥
{ expiresIn: 86400 } // 过期时间:24小时
);
// 3. 返回 token 和用户信息
return {
code: 0,
user: { username: body.username },
token: token
};
}
}
];
核心要点:
jwt.sign()- 签发 Token,将用户信息加密jwt.verify()- 验证 Token,解密获取用户信息- Token 格式:
Header.Payload.Signature(三段式)
2️⃣ 状态管理层:Zustand 全局状态
src/store/user.js 负责管理用户登录状态:
javascript
import { create } from "zustand";
export const useAuthStore = create(set => ({
// 初始状态:从 localStorage 恢复(实现持久化)
token: localStorage.getItem("token") || '',
user: JSON.parse(localStorage.getItem("user")) || null,
// 登录:保存 token 和用户信息
setAuth: ({ token, user }) => {
localStorage.setItem("token", token);
localStorage.setItem("user", JSON.stringify(user));
set({ token, user });
},
// 登出:清除所有状态
logout: () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
set({ token: '', user: null });
}
}));
Zustand 的优势:
- 无需 Provider :直接在组件中使用
useAuthStore - 简单直观 :
create创建 store,set修改状态 - 自动订阅:状态变化时组件自动重渲染
3️⃣ 网络请求层:Axios 拦截器
src/api/config.js 配置 Axios 实例和拦截器:
javascript
import axios from 'axios';
// 创建 Axios 实例
const instance = axios.create({
baseURL: '/api',
timeout: 5000
});
// 请求拦截器:自动添加 Token
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
// 在请求头中添加 Authorization
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一处理返回数据
instance.interceptors.response.use(res => {
return res.data; // 直接返回 data,简化调用
});
export default instance;
拦截器的作用:
- 请求拦截器:在每个请求发出前,自动添加 Token 到请求头
- 响应拦截器:统一处理返回数据,简化业务代码
4️⃣ 路由守卫:保护需要鉴权的页面
src/components/RequireAuth.jsx 实现路由保护:
jsx
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../store/user';
export default function RequireAuth({ children }) {
const token = useAuthStore(state => state.token);
// 没有 token,重定向到登录页
if (!token) {
return <Navigate to="/login" replace />;
}
// 有 token,渲染子组件
return <>{children}</>;
}
使用方式:
jsx
// App.jsx
<Route path="/pay" element={
<RequireAuth>
<Pay />
</RequireAuth>
} />
5️⃣ 登录页面:完整的登录流程
src/pages/Login.jsx 实现登录表单和登录逻辑:
jsx
import { useState } from 'react';
import { login } from '../api/user';
import { useAuthStore } from '../store/user';
import { useNavigate } from 'react-router-dom';
export default function Login() {
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('123456');
const [err, setErr] = useState('');
const [loading, setLoading] = useState(false);
const setAuth = useAuthStore(s => s.setAuth);
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setErr('');
setLoading(true);
try {
// 1. 调用登录接口
const res = await login({ username, password });
// 2. 检查返回结果
if (res.code !== 0) {
setErr(res.message || '登录失败');
return;
}
// 3. 保存认证信息到 Store
setAuth({ token: res.token, user: res.user });
// 4. 跳转到首页
navigate('/');
} catch (e) {
setErr(e?.message || '网络错误');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>登录</h2>
<label>
用户名:
<input value={username} onChange={e => setUsername(e.target.value)} />
</label>
<label>
密码:
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
</label>
{err && <div style={{ color: 'red' }}>{err}</div>}
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
<div>默认 admin / 123456</div>
</form>
);
}
6️⃣ 导航组件:根据登录状态显示不同内容
src/components/Nav.jsx:
jsx
import { useAuthStore } from '../store/user';
import { Link } from 'react-router-dom';
export default function Nav() {
const token = useAuthStore(state => state.token);
const user = useAuthStore(state => state.user);
const logout = useAuthStore(state => state.logout);
return (
<nav>
<Link to="/">Home</Link>
<Link to="/pay">Pay</Link>
{/* 未登录显示 Login 链接 */}
{!token && <Link to="/login">Login</Link>}
{/* 已登录显示用户名和登出按钮 */}
{user && <a>{user.username}</a>}
{token && <button onClick={logout}>Logout</button>}
</nav>
);
}
🔄 完整鉴权流程图
scss
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户 │ │ Login │ │ Store │ │ Mock │
│ 输入 │ │ 页面 │ │ Zustand │ │ 服务器 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ 输入账号密码 │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ POST /login │ │
│ │───────────────────────────────>│
│ │ │ │
│ │ │ 验证账号密码 │
│ │ │ 签发 JWT Token │
│ │ │ │
│ │ { token, user }│ │
│ │<───────────────────────────────│
│ │ │ │
│ │ setAuth() │ │
│ │───────────────>│ │
│ │ │ │
│ │ │ 保存到 localStorage
│ │ │ 更新 Store 状态
│ │ │ │
│ 跳转首页 │ │ │
│<───────────────│ │ │
│ │ │ │
│ 访问受保护页面 │ │ │
│────────────────────────────────────────────────>│
│ │ │ │
│ │ │ 拦截器自动 │
│ │ │ 添加 Token │
│ │ │ │
│ │ │ 验证 Token │
│ │ │ 解码用户信息 │
│ │ │ │
│ 返回数据 │ │ │
│<────────────────────────────────────────────────│
🐛 踩坑记录(血泪教训)
这三个坑我都踩过,当时调试了半天才发现问题,分享出来希望你们能少走弯路!
问题 1:Token 格式错误(最常见!)
现象:请求接口返回 401,提示 "Invalid token"
原因 :Authorization header 格式应该是 Bearer <token>,不是直接传 token
解决:
javascript
// ❌ 错误写法
config.headers['Authorization'] = token;
// ✅ 正确写法
config.headers['Authorization'] = `Bearer ${token}`;
问题 2:页面刷新后状态丢失
现象:登录成功后刷新页面,又变成未登录状态
原因:Zustand 状态存储在内存中,刷新后丢失
解决:结合 localStorage 实现持久化
javascript
export const useAuthStore = create(set => ({
// 初始化时从 localStorage 读取
token: localStorage.getItem("token") || '',
user: JSON.parse(localStorage.getItem("user")) || null,
setAuth: ({ token, user }) => {
// 保存到 localStorage
localStorage.setItem("token", token);
localStorage.setItem("user", JSON.stringify(user));
set({ token, user });
},
}));
问题 3:响应拦截器导致数据获取错误
现象 :await axios.post('/login', data) 返回的数据结构不对
原因 :响应拦截器已经返回了 res.data,不需要再取 .data
解决:
javascript
// ❌ 错误写法
const res = await axios.post('/login', data);
console.log(res.data.token); // undefined
// ✅ 正确写法
const res = await axios.post('/login', data);
console.log(res.token); // 正确获取
💡 核心知识点总结
1. JWT 工作原理
diff
签发:JSON 对象 → 加密 → Token 字符串
验证:Token 字符串 → 解密 → JSON 对象
Token 结构:Header.Payload.Signature
- Header:算法和类型
- Payload:用户信息(可解码,不要放敏感数据)
- Signature:签名(防止篡改)
2. Zustand 状态管理
javascript
// 创建 Store
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
// 在组件中使用
function Component() {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
}
3. Axios 拦截器
javascript
// 请求拦截器:修改请求配置
instance.interceptors.request.use(config => {
config.headers['Authorization'] = `Bearer ${token}`;
return config;
});
// 响应拦截器:处理响应数据
instance.interceptors.response.use(res => {
return res.data;
});
4. 路由守卫模式
jsx
// 包裹需要保护的路由
<Route path="/protected" element={
<RequireAuth>
<ProtectedPage />
</RequireAuth>
} />
🔗 项目依赖
json
{
"dependencies": {
"axios": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.18.2",
"zustand": "^5.0.15"
},
"devDependencies": {
"vite-plugin-mock": "^3.0.2"
}
}
🔗 参考资料
- JWT 官方文档 - 在线调试 Token
- Zustand 官方文档 - 状态管理神器
- Axios 官方文档 - HTTP 请求库
- React Router 官方文档 - 路由管理
💬 写在最后
这个 Demo 花了我周末两天时间整理,把之前项目中的鉴权方案抽离出来,希望能帮到大家。
核心收获:
- JWT 不是玄学,就是把用户信息加密后传来传去
- Zustand 真的比 Redux 简单太多,强烈推荐!
- Axios 拦截器是鉴权的关键,一定要理解
🤔 还可以继续深入的方向:
- Token 自动续期(Refresh Token 机制)
- 基于角色的动态路由权限
- 多标签页状态同步
- OAuth 2.0 第三方登录
🔥 下一篇预告:手把手实现 Token 自动续期 + 权限动态路由,关注不迷路!
有问题欢迎评论区交流,我会一一回复~
觉得有用?三连就是最大的支持! 👍⭐👆
作者:你的掘金用户名 链接:文章链接 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。