news 2026/8/24 15:02:42

React + Zustand + JWT:从零实现登录鉴权与请求拦截

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React + Zustand + JWT:从零实现登录鉴权与请求拦截

引言

你有没有遇到过这样的场景:用户登录成功后,刷新页面就丢了登录态?或者每个请求都要手动写一遍Authorization头,代码又臭又长?

说白了,登录鉴权的本质,就是在无状态的 HTTP 协议上,建立起一个可信任的身份传递机制。今天这篇文章,我会用 React + Zustand + Axios + JWT,带你从零搭建一个完整的前端登录鉴权方案,并且用 mockjs 模拟后端接口,让你读完就能直接落地到自己的项目里。

读完你将收获:

  • JWT 的 sign / verify 核心原理
  • Zustand 管理全局登录态的实战用法
  • Axios 拦截器自动携带 token 的优雅实现
  • 基于 mockjs 的本地后端模拟
  • 路由守卫与导航栏登录态展示的完整思路

一、JWT:无状态 HTTP 的身份令牌

HTTP 是无状态的,每次请求都是独立的,服务器不知道你是谁。所以我们需要一个“身份凭证”,让用户在登录后,后续每次请求都能证明“我是 admin”。

JWT(JSON Web Token)就是为此而生的。它本质上是一个字符串,由三部分组成:

Header.Payload.Signature
  • Header:声明加密算法(如 HS256)
  • Payload:存放用户身份信息(JSON 对象)
  • Signature:对前两部分的签名,防止篡改

JWT 有两个核心动作:

  • sign:把用户 JSON 对象加密成 token(登录时颁发)
  • verify:把 token 解密还原成 JSON 对象(后续请求验证)

对比传统的 cookie/session 方案,JWT 最大的优势是无状态

  • session 方案:服务器需要保存 sessionId 和会话对象,分布式环境下要共享 session,麻烦。
  • JWT 方案:任何一台服务器签发的 token,在其他服务器上都能用同一个 secret 验签,天然适合微服务和分布式架构。

下面看我们 mock 后端如何实现 sign 和 verify:

// mock/user.js 中的登录接口(部分) const token = jwt.sign( { user: body.username, // payload 放用户信息 role: 'admin' }, secret, // 加密密钥(生产环境请放环境变量) { expiresIn: 86400 // 过期时间 24h } )
// mock/user.js 中的 /api/repo 接口(验证 token) const authHeader = req.headers?.authorization; if (!authHeader) { return { code: 401, msg: 'Missing authorization header' }; } const token = authHeader.split(' ')[1]; // Bearer xxx try { let decoded = jwt.verify(token, secret); // decoded 就是之前 sign 进去的 payload return { code: 0, data: decoded.user } } catch (err) { return { code: 401, msg: 'Invalid token' } }

你会发现,JWT 就像一个自带身份信息的加密信封,前端拿到后每次请求带上它,后端验签通过就知道你是谁。


二、Zustand:轻量全局状态管理

登录状态(token、user 信息)是典型的跨组件、跨路由共享的数据。如果用 React 自带的 Context 也能做,但代码会显得啰嗦;用 Redux 又太重。Zustand 就是一个非常轻量的选择——API 简单,不需要 Provider 包裹,直接 hooks 调用。

全局状态不要散落在组件里,统一交给 store,组件只负责渲染和交互。

我们创建store/user.js

import { create } from 'zustand'; export const useAuthStore = create(set => ({ // 初始化时从 localStorage 读取,解决刷新丢状态问题 token: localStorage.getItem('token') || '', user: JSON.parse(localStorage.getItem('user')) || null, // 登录成功后设置状态,并持久化到 localStorage 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 }); } }))

在组件中使用:

const setAuth = useAuthStore(state => state.setAuth); const user = useAuthStore(state => state.user); const token = useAuthStore(state => state.token); const logout = useAuthStore(state => state.logout);

Zustand 的create返回一个 hook,组件里按需订阅状态,避免不必要的渲染。这就是它比 Context 更优雅的地方。

深入理解 set 方法

上面的代码里,create的函数参数接收一个set,这个set就是 Zustand 用来更新状态的核心方法。理解它,你就掌握了 Zustand 的精髓。

set的用法非常灵活,主要有两种形式:

1. 直接传入一个对象(部分状态)

set({ token: 'xxx', user: { name: 'admin' } })

这是最常见的写法,Zustand 会浅合并这个对象到当前状态中。也就是说,你只传需要更新的字段,其他字段保持不变。比如setAuth里我们只更新了tokenuser,如果 store 里还有其他状态(比如count),它们不会被影响。

2. 传入一个函数,基于旧状态计算新状态

set(state => ({ count: state.count + 1 }))

当你需要依赖旧状态来更新时,一定要用函数形式。比如计数器、数组追加等场景,避免因闭包导致的状态过期问题。

在我们的store/user.js中,使用的是对象形式,因为setAuthlogout都是整体替换tokenuser,不依赖旧值。但如果你的 store 有更复杂的更新逻辑,函数形式会更安全。

另外,注意set同步执行的,调用后 Zustand 会立即通知所有订阅了相关状态的组件重新渲染。所以不需要await,也不要在set后面写依赖新状态的同步逻辑(不过实际上状态已经更新了,可以读取到最新值)。

再看另一个例子store/todos.js

import { create } from 'zustand'; export const useTodosStore = create(set => ({ todos: [], setTodos: (todos) => { set({ todos }); } }))

这里setTodos也是用对象形式整体替换todos数组。如果要在原数组上追加,就可以改成函数形式:

addTodo: (todo) => set(state => ({ todos: [...state.todos, todo] }))

一句话总结:set是 Zustand 的状态更新入口,对象形式用于普通更新,函数形式用于依赖旧状态的更新。掌握它,你的 store 就能灵活应对各种场景。


三、mockjs:前端独立开发的后端模拟器

前后端分离开发时,后端接口可能还没好。用vite-plugin-mock可以轻松在本地模拟接口,让你专注于前端逻辑。

安装配置:

// vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { viteMockServe } from 'vite-plugin-mock' export default defineConfig({ plugins: [ react(), viteMockServe({ mockPath: 'mock', // mock 文件目录 localEnabled: true // 本地开发启用 }) ], })

然后在mock/user.js中定义接口。我们有两个接口:

  • POST /api/login:验证用户名密码,签 token
  • GET /api/repo:需要携带 token,验签后返回数据

完整的 mock 代码上面已经展示过了,这里不再重复。需要注意的是,mock 接口的路径要和 axios 的 baseURL 匹配,我们后面会设置baseURL: '/api',这样请求/login实际就是/api/login


四、登录流程实战:表单验证 + 状态更新

登录页面是鉴权的入口,我们来看核心逻辑:

// pages/Login.jsx 关键部分 import React, { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { login } from '../api/user'; import { useAuthStore } from '../store/user'; function Login() { const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from || '/'; // 记录来源页面 const setAuth = useAuthStore(state => state.setAuth); const [formData, setFormData] = useState({ username: '', password: '' }); const [errors, setErrors] = useState({ username: '', password: '' }); const [isValid, setIsValid] = useState(false); // 表单验证逻辑(略) const handleLogin = async e => { e.preventDefault(); try { const res = await login(formData); // 调用 API if (res.code === 0) { setAuth({ token: res.token, user: res.user }); // 存入全局状态 navigate(from, { replace: true }); // 跳转回原页面 } else { alert(res.message || '登录失败'); } } catch (err) { console.error(err); alert('登录失败'); } }; // 渲染表单(略) }

登录成功后,token 和 user 同时写入了 localStorage 和 Zustand store。这样即使用户刷新页面,也能从 localStorage 恢复登录态。

注意:真实项目中,res.user应该由后端返回完整用户信息,不要只信任前端传的用户名


五、Axios 拦截器:自动携带 token

如果每次请求都要手动写:

axios.get('/repo', { headers: { authorization: `Bearer ${token}` } })

那代码会非常重复且容易遗漏。Axios 拦截器就是来解决这个问题的。

// api/config.js import axios from 'axios'; const instance = axios.create({ baseURL: '/api', timeout: 5000 }); // 请求拦截器:每个请求发出前,自动附加 token instance.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['authorization'] = `Bearer ${token}`; } return config; }); // 响应拦截器:直接返回 res.data,简化调用 instance.interceptors.response.use(res => { return res.data; }); export default instance;

拦截器就像一道安检门,每个请求都要经过它——请求前检查有没有 token,有就带上;响应后统一解包,业务代码直接拿数据。

这样,业务 API 函数就变得非常干净:

// api/repo.js import axios from './config'; export const getRepo = async () => { const res = await axios.get('/repo'); return res; }

六、路由守卫:保护敏感页面

有些页面(如支付页/pay)必须登录后才能访问。我们需要一个路由守卫组件:

// components/RequireAuth.jsx import { Navigate } from 'react-router-dom'; import { useAuthStore } from '../store/user'; function RequireAuth({ children }) { const token = useAuthStore(state => state.token); if (!token) { return <Navigate to="/login" replace />; } return <>{children}</>; } export default RequireAuth;

然后在路由中包裹需要保护的页面:

// App.jsx <Route path="/pay" element={ <RequireAuth> <Pay /> </RequireAuth> } />

这样,未登录用户访问/pay就会被重定向到/login

进阶优化:如果你希望登录后能跳回用户原本想访问的页面,可以在RequireAuth中传递state

// 改进版 RequireAuth import { Navigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../store/user'; function RequireAuth({ children }) { const token = useAuthStore(state => state.token); const location = useLocation(); if (!token) { return <Navigate to="/login" state={{ from: location }} replace />; } return <>{children}</>; }

这样登录页中的location.state?.from就能拿到原始路径,实现“从哪里来,回哪里去”的体验。


七、导航栏:登录态的条件渲染

导航栏是展示登录状态的绝佳位置。根据 token 和 user 的不同,显示不同的链接和按钮:

// components/Nav.jsx import { Link } from 'react-router-dom'; import { useAuthStore } from '../store/user'; function Nav() { const token = useAuthStore(state => state.token); const user = useAuthStore(state => state.user); const logout = useAuthStore(state => state.logout); const handleLogout = () => { logout(); // 可选:跳转到首页 // window.location.href = '/'; }; return ( <nav style={{ padding: 0, borderBottom: '1px solid #ccc' }}> <Link to="/">Home</Link> <Link to="/pay">Pay</Link> {!token && <Link to="/login">Login</Link>} {user && <a>{user.name}</a>} {token && <button onClick={handleLogout}>Logout</button>} </nav> ); } export default Nav;

登录态的管理,不仅仅是为了鉴权,更是为了提供个性化的用户体验。导航栏根据 token 和 user 动态展示,让用户一眼就知道自己是否登录、是谁。


八、完整流程串联

让我们把整个流程串起来:

  1. 用户登录:输入用户名密码,提交/api/login
  2. 后端签发 token:验证通过后,用jwt.sign生成 token 并返回
  3. 前端存储状态setAuth将 token 和 user 存入 Zustand store 和 localStorage
  4. 后续请求自动带 token:Axios 请求拦截器从 localStorage 读取 token,写入authorization
  5. 后端验证 token:需要鉴权的接口用jwt.verify验签,返回用户数据
  6. 路由守卫保护页面:未登录访问敏感页面自动跳转登录
  7. 导航栏动态展示:根据登录态显示不同内容

整个方案的核心思想是:状态统一管理,请求统一拦截,页面统一守卫


九、避坑指南与总结

常见坑点

  1. localStorage 的 XSS 风险:token 存在 localStorage 中,如果网站存在 XSS 漏洞,攻击者可以窃取 token。更安全的做法是使用 httpOnly cookie,但那需要后端配合设置。对于多数内部系统或学习项目,localStorage 足够方便。
  2. token 过期处理:JWT 有过期时间(expiresIn),过期后后端会返回 401。前端可以在响应拦截器中统一处理 401,比如清除状态并跳转登录页。本文示例没有处理,实际项目中应该加上。
  3. 刷新页面状态初始化:Zustand 初始化时从 localStorage 读取,这是同步操作,不会导致组件初始渲染时状态缺失。但如果用户信息很大,可以考虑只存 token,用户信息通过接口获取。
  4. secret 安全:mock 中的 secret 是硬编码的,生产环境一定要放在环境变量中,并且定期更换。

总结

我们用一套简洁的技术栈(React + Zustand + Axios + JWT)实现了完整的登录鉴权流程。核心代码覆盖了:

  • JWT 的 sign / verify
  • Zustand 全局状态管理(含set方法解析)
  • Axios 请求/响应拦截器
  • mockjs 本地模拟接口
  • 路由守卫保护页面
  • 导航栏登录态展示

技术选型没有银弹,但适合自己的就是最好的。Zustand 足够轻,JWT 足够通用,Axios 拦截器足够优雅,这套组合拳非常适合中小型 React 项目快速落地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/24 15:01:36

3步完成QQ空间说说全量备份:GetQzonehistory历史备份完整指南

3步完成QQ空间说说全量备份&#xff1a;GetQzonehistory历史备份完整指南 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 如果你的QQ空间攒了多年说说和旧照片&#xff0c;别等哪天找不…

作者头像 李华
网站建设 2026/8/24 15:01:02

字节阿里腾讯校招AI岗位超多?7个真相揭示AI行业反直觉机会

2026年校招中&#xff0c;字节、阿里、腾讯等大厂AI岗位激增&#xff0c;但行业呈现反直觉趋势&#xff1a;开放岗位门槛极高&#xff0c;本科生或胜过硕士生&#xff0c;底层系统工程师薪资追平研究员&#xff0c;CV/NLP岗位被大模型整合&#xff0c;游戏公司AI投入激进&#…

作者头像 李华
网站建设 2026/8/24 14:57:17

Landsat数据—6s大气校正

Landsat数据认识—以及6s大气校正处理1. Landsat卫星介绍2. Landsat数据介绍2.1 数据级别介绍2.2 数据名称命名格式3. Landsat数据波段信息介绍3.1 Landsat 4-5 Thematic Mapper ( TM )3.2 Landsat 7 Enhanced Thematic Mapper Plus (ETM)3.3 Landsat 8-9 Operational Land Ima…

作者头像 李华
网站建设 2026/8/24 14:57:04

利用Anaconda和Mamba创建深度学习常用三大环境

文章目录深度学习常用研发项目及所需依赖包准备工作环境一&#xff1a;大模型训练/推理 Web后端环境二&#xff1a;视觉检测 姿态识别 标注环境三&#xff1a;autogluon自动化学习环境部署深度学习常用研发项目及所需依赖包 &#xff08;1&#xff09;autogluon自动化学习需…

作者头像 李华
网站建设 2026/8/24 14:56:23

讯飞输入法重磅发布11.0版本 懂输入更懂你

2010年, 中国互联网借助全球移动互联网的浪潮, 诞生了一批又一批所谓“新生力量”。在输入法的历史进程中, 讯飞输入法为行业开创中文语音输入, 一个里程碑就此出现。正是因为这一创新性举措, 人机交互方式得以翻开全新篇章。日前, 讯飞输入法将前沿AI技术全面进行赋能, 重磅推…

作者头像 李华
网站建设 2026/8/24 14:53:28

AI 软件开发实战教程(十二):安全地交换双方微信号

“AI 软件开发实战教程”系列第 12 篇&#xff1a;让候选中的一方主动确认后&#xff0c;双方同时获得对方微信号&#xff0c;同时保证确认前不泄露、重复点击不重复、资料修改不改写历史。 系统找到“可能同路”的人以后&#xff0c;真正的沟通仍要回到微信。邻行不做站内聊天…

作者头像 李华