前言:在 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 应用。
-
关键点:
- Token 存储 :使用
zustand/middleware
持久化。 - Axios 拦截器:自动注入 Token + 处理 401 错误。
- 安全优化:可增加 Token 自动刷新、请求重试。
- Token 存储 :使用
-
完整示例 :GitHub Repo
这样,你的 React 应用就能安全、高效地管理 Token 了! 🚀