Zustand + Axios 存储 Token 最佳实践

前言:在 React 应用中,通常需要管理用户登录状态和 Token,并确保每次请求都携带 Token。结合 Zustand(状态管理) 和 Axios(HTTP 请求) ,可以实现高效、安全的 Token 存储和自动注入。

1. 方案设计

核心目标

  • ✅ Token 存储 :登录后存储 Token,并持久化(如 localStorage)
  • ✅ 自动注入 Token:每次请求自动携带 Token
  • ✅ Token 过期处理:拦截 401 错误,跳转登录页
  • ✅ 全局状态管理:用户登录状态共享

技术栈

  • Zustand:管理 Token 和用户状态
  • Axios:封装 HTTP 请求,拦截器自动注入 Token
  • 持久化中间件 :zustand/middleware 持久化 Token

2. 代码实现

2.1 安装依赖

csharp 复制代码
npm install zustand axios
# 或
yarn add zustand axios
# 或
pnpm add zustand axios

2.2 创建 Zustand Store(管理 Token 和用户状态)

typescript 复制代码
// store/authStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

type AuthState = {
  token: string | null;
  isAuthenticated: boolean;
  login: (token: string) => void;
  logout: () => void;
};

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      token: null,
      isAuthenticated: false,
      login: (token: string) => set({ token, isAuthenticated: true }),
      logout: () => set({ token: null, isAuthenticated: false }),
    }),
    {
      name: "auth-storage", // localStorage 的 key
    }
  )
);

2.3 封装 Axios(自动携带 Token + 401 拦截)

javascript 复制代码
// utils/api.ts
import axios from "axios";
import { useAuthStore } from "../store/authStore";

const api = axios.create({
  baseURL: "https://your-api.com",
});

// 请求拦截器:自动注入 Token
api.interceptors.request.use((config) => {
  const { token } = useAuthStore.getState();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:处理 401 错误(Token 过期)
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout(); // 清除 Token
      window.location.href = "/login"; // 跳转登录页
    }
    return Promise.reject(error);
  }
);

export default api;

2.4 使用示例

(1) 登录(存储 Token)

javascript 复制代码
// Login.tsx
import { useState } from "react";
import { useAuthStore } from "../store/authStore";
import api from "../utils/api";

function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const login = useAuthStore((state) => state.login);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const res = await api.post("/auth/login", { email, password });
      login(res.data.token); // 存储 Token
    } catch (error) {
      alert("Login failed");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}

(2) 受保护路由(检查登录状态)

javascript 复制代码
// Dashboard.tsx
import { useEffect } from "react";
import { useAuthStore } from "../store/authStore";
import api from "../utils/api";

function Dashboard() {
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  useEffect(() => {
    if (!isAuthenticated) {
      window.location.href = "/login";
    }
  }, [isAuthenticated]);

  const fetchData = async () => {
    try {
      const res = await api.get("/user/profile"); // 自动携带 Token
      console.log(res.data);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={fetchData}>Load Data</button>
    </div>
  );
}

(3) 登出(清除 Token)

javascript 复制代码
// LogoutButton.tsx
import { useAuthStore } from "../store/authStore";

function LogoutButton() {
  const logout = useAuthStore((state) => state.logout);

  return <button onClick={logout}>Logout</button>;
}

3. 优化点

(1) Token 自动刷新(JWT)

如果 Token 过期时间较短,可以在拦截器中自动刷新:

ini 复制代码
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const res = await api.post("/auth/refresh-token");
        useAuthStore.getState().login(res.data.token); // 更新 Token
        return api(originalRequest); // 重试请求
      } catch (err) {
        useAuthStore.getState().logout();
        window.location.href = "/login";
      }
    }
    return Promise.reject(error);
  }
);

(2) 类型安全(TypeScript)

typescript 复制代码
// 增强 Axios 请求类型
declare module "axios" {
  interface AxiosRequestConfig {
    _retry?: boolean; // 用于 Token 刷新
  }
}

(3) 测试 Mock

php 复制代码
// 测试时 Mock Axios
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
mockedAxios.post.mockResolvedValue({ data: { token: "fake-token" } });

4. 对比 Redux + Redux-Thunk

方案 Zustand + Axios Redux + Redux-Thunk
代码量 更简洁(~50行) 需要 actions/reducers(~100行)
性能 更高(按需更新) 依赖 useSelector 优化
异步管理 直接使用 async/await 需要 createAsyncThunk
持久化 内置支持(persist) 需要 redux-persist
适用场景 中小型应用 大型复杂应用

5. 总结

  • 推荐使用 Zustand + Axios:代码更简洁,适合大多数 React 应用。

  • 关键点:

    1. Token 存储 :使用 zustand/middleware 持久化。
    2. Axios 拦截器:自动注入 Token + 处理 401 错误。
    3. 安全优化:可增加 Token 自动刷新、请求重试。
  • 完整示例 :GitHub Repo

这样,你的 React 应用就能安全、高效地管理 Token 了! 🚀

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端