🔐 前端鉴权不再难!手把手带你搞懂 JWT + Zustand + Axios 的完整登录方案

0 阅读6分钟

🔐 前端鉴权不再难!手把手带你搞懂 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

用户登录 → 服务器创建 Session → 返回 Cookie(sessionId)
   ↓
用户再次请求 → 携带 Cookie → 服务器查找 Session → 认证成功

问题

  • Session 存储在服务器内存,不适合分布式部署
  • Cookie 每次请求都会自动携带,有 CSRF 风险

现代方案:JWT (JSON Web Token)

JWT 的核心思想:把用户身份信息加密后直接发给客户端,客户端每次请求时带上这个令牌

用户登录 → 服务器签发 Token → 客户端存储
   ↓
用户再次请求 → 携带 Token → 服务器验证并解码 → 认证成功

优势

  • 无状态,服务器不需要存储 Session
  • 支持分布式部署,任何服务器都能验证 Token
  • 可以携带自定义数据(用户角色、权限等)

🔧 技术方案设计

整体架构

┌─────────────────────────────────────────────────────────┐
│                      前端应用                             │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│  │ Login   │  │  Home   │  │   Pay   │  │   Nav   │    │
│  └────┬────┘  └─────────┘  └────┬────┘  └────┬────┘    │
│       │                         │             │         │
│       ▼                         ▼             ▼         │
│  ┌─────────────────────────────────────────────────┐    │
│  │              Zustand Store (user.js)             │    │
│  │         管理 token、user 全局状态                 │    │
│  └─────────────────────────────────────────────────┘    │
│       │                                                  │
│       ▼                                                  │
│  ┌─────────────────────────────────────────────────┐    │
│  │              Axios 实例 (config.js)              │    │
│  │     请求拦截器:自动添加 Authorization header      │    │
│  │     响应拦截器:统一处理返回数据                   │    │
│  └─────────────────────────────────────────────────┘    │
│       │                                                  │
└───────┼─────────────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│                  Mock 服务器                              │
│  ┌─────────────────┐  ┌─────────────────┐              │
│  │   /api/login    │  │   /api/repo     │              │
│  │   签发 Token    │  │   验证 Token    │              │
│  └─────────────────┘  └─────────────────┘              │
└─────────────────────────────────────────────────────────┘

技术栈选择

技术作用为什么选它
React 19UI 框架主流前端框架,生态成熟
Zustand状态管理轻量、简单、无 Provider
AxiosHTTP 请求支持拦截器、功能强大
vite-plugin-mockMock 数据本地开发无需后端
jsonwebtokenJWT 签发验证模拟后端 JWT 逻辑
💭 我为什么选 Zustand 而不是 Redux?

说实话,之前我也用 Redux,但光配置就要写一堆:

// Redux 的 boilerplate 真的多...
const ActionTypes = { LOGIN: 'LOGIN', LOGOUT: 'LOGOUT' };
const loginAction = (user) => ({ type: ActionTypes.LOGIN, payload: user });
const authReducer = (state, action) => { /* ... */ };
// 还要 createStore、Provider 包裹...

换了 Zustand 后,一个 create 搞定:

// Zustand 简洁多了!
const useAuthStore = create(set => ({
    token: '',
    setAuth: (token) => set({ token }),
}));
// 直接用,不需要 Provider!

代码量直接减少 60%,而且学习成本极低,5 分钟上手!

当然,Redux 也有它的优势(中间件、DevTools),但对中小项目来说,Zustand 完全够用,而且更香!

📝 代码实现详解

1️⃣ Mock 层:模拟后端 JWT 签发和验证

mock/user.js 是整个鉴权流程的起点,模拟后端的登录和鉴权接口:

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 负责管理用户登录状态:

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 实例和拦截器:

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 实现路由保护:

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}</>;
}

使用方式

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

5️⃣ 登录页面:完整的登录流程

src/pages/Login.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

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>
    );
}

🔄 完整鉴权流程图

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  用户    │     │ 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

解决

// ❌ 错误写法
config.headers['Authorization'] = token;

// ✅ 正确写法
config.headers['Authorization'] = `Bearer ${token}`;

问题 2:页面刷新后状态丢失

现象:登录成功后刷新页面,又变成未登录状态

原因:Zustand 状态存储在内存中,刷新后丢失

解决:结合 localStorage 实现持久化

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

解决

// ❌ 错误写法
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 工作原理

签发:JSON 对象 → 加密 → Token 字符串
验证:Token 字符串 → 解密 → JSON 对象

Token 结构:Header.Payload.Signature
- Header:算法和类型
- Payload:用户信息(可解码,不要放敏感数据)
- Signature:签名(防止篡改)

2. Zustand 状态管理

// 创建 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 拦截器

// 请求拦截器:修改请求配置
instance.interceptors.request.use(config => {
    config.headers['Authorization'] = `Bearer ${token}`;
    return config;
});

// 响应拦截器:处理响应数据
instance.interceptors.response.use(res => {
    return res.data;
});

4. 路由守卫模式

// 包裹需要保护的路由
<Route path="/protected" element={
    <RequireAuth>
        <ProtectedPage />
    </RequireAuth>
} />

🔗 项目依赖

{
  "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"
  }
}

🔗 参考资料


💬 写在最后

这个 Demo 花了我周末两天时间整理,把之前项目中的鉴权方案抽离出来,希望能帮到大家。

核心收获

  1. JWT 不是玄学,就是把用户信息加密后传来传去
  2. Zustand 真的比 Redux 简单太多,强烈推荐!
  3. Axios 拦截器是鉴权的关键,一定要理解

🤔 还可以继续深入的方向

  • Token 自动续期(Refresh Token 机制)
  • 基于角色的动态路由权限
  • 多标签页状态同步
  • OAuth 2.0 第三方登录

🔥 下一篇预告:手把手实现 Token 自动续期 + 权限动态路由,关注不迷路!

有问题欢迎评论区交流,我会一一回复~


觉得有用?三连就是最大的支持! 👍⭐👆

作者:[你的掘金用户名] 链接:[文章链接] 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。