一个企业级完整的 React AuthProvider + 自动刷新 AccessToken + 过期退出 示例。
核心思路:
-
AccessToken 有效期短(例如 30 分钟)
-
RefreshToken 有效期长(例如 7 天)
-
前端 UI 状态与后端权限保持一致
-
AccessToken 过期 → 自动刷新
-
RefreshToken 过期 → 自动退出登录
下面是完整示例:
import React, { createContext, useState, useEffect, useContext } from 'react';
import axios from 'axios';
import { login as loginApi, logout as logoutApi, refreshToken as refreshTokenApi } from '../api/auth';
export const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// 初始化:检查 token + refresh token
useEffect(() => {
const initAuth = async () => {
const storedToken = localStorage.getItem('token');
const storedRefreshToken = localStorage.getItem('refreshToken');
const storedUser = localStorage.getItem('user');
if (!storedToken || !storedRefreshToken || !storedUser) {
setLoading(false);
return;
}
setToken(storedToken);
setUser(JSON.parse(storedUser));
setIsAuthenticated(true);
// 这里可以尝试刷新 token,如果已过期
try {
await tryRefreshToken();
} catch (error) {
logout();
}
setLoading(false);
};
initAuth();
}, []);
// 登录
const login = async (credentials) => {
const response = await loginApi(credentials);
const { code, message, data } = response;
if (code !== 200) {
setIsAuthenticated(false);
setUser(null);
setToken(null);
throw new Error(message || '登录失败');
}
const { token: accessToken, refreshToken, userType, userName } = data;
const userData = { userType, userName };
setToken(accessToken);
setUser(userData);
setIsAuthenticated(true);
localStorage.setItem('token', accessToken);
localStorage.setItem('refreshToken', refreshToken);
localStorage.setItem('user', JSON.stringify(userData));
};
// 登出
const logout = async () => {
try {
await logoutApi();
} catch (error) {
console.error('登出API失败', error);
} finally {
setToken(null);
setUser(null);
setIsAuthenticated(false);
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
}
};
// 尝试刷新 token
const tryRefreshToken = async () => {
const storedRefreshToken = localStorage.getItem('refreshToken');
if (!storedRefreshToken) throw new Error('没有 refresh token');
try {
const res = await refreshTokenApi({ refreshToken: storedRefreshToken });
const { accessToken, refreshToken } = res.data;
setToken(accessToken);
localStorage.setItem('token', accessToken);
localStorage.setItem('refreshToken', refreshToken);
} catch (error) {
throw new Error('Refresh token 已过期');
}
};
// Axios 拦截器,自动刷新 token 或退出登录
useEffect(() => {
const interceptor = axios.interceptors.response.use(
(res) => res,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await tryRefreshToken();
originalRequest.headers['Authorization'] = 'Bearer ' + localStorage.getItem('token');
return axios(originalRequest);
} catch (e) {
logout();
return Promise.reject(e);
}
}
return Promise.reject(error);
}
);
return () => {
axios.interceptors.response.eject(interceptor);
};
}, []);
const value = {
user,
token,
loading,
isAuthenticated,
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuthStore = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuthStore 必须在 AuthProvider 内使用');
return context;
};
✅ 效果说明
-
页面刷新 → 尝试恢复 token,并刷新一次 AccessToken,如果 RefreshToken 有效
-
AccessToken 过期 → 自动刷新 token,无需登出
-
RefreshToken 过期 → 前端自动登出,并清理 localStorage
-
所有 axios 请求 401 → 自动刷新或登出
前端 RefreshToken 自动续签 + AccessToken 过期退出逻辑 的完整流程
JWT + RefreshToken 生命周期图
用户登录
│
▼
后端返回:
{
accessToken (30分钟),
refreshToken (7天)
}
│
├─ 前端保存:
│ localStorage.token = accessToken
│ localStorage.refreshToken = refreshToken
│ localStorage.user = userData
▼
前端正常访问接口 (带 AccessToken)
│
▼
后端验证 AccessToken
├─ 有效 → 返回数据
└─ 过期 → 返回 401
│
▼
前端 Axios 拦截器捕获 401
│
▼
尝试刷新 AccessToken (用 RefreshToken)
│
┌──────┴────────┐
│ │
RefreshToken 有效 RefreshToken 过期
│ │
▼ ▼
后端返回新 AccessToken 前端登出
│
▼
前端保存新 AccessToken
│
▼
重试原请求 → 用户无感知
🔹 说明
-
AccessToken(短期)
-
用于每次接口访问
-
过期快(30分钟)
-
过期 → 自动刷新
-
-
RefreshToken(长期)
-
用于刷新 AccessToken
-
过期慢(7天)
-
过期 → 强制登出
-
-
前端状态逻辑
-
isAuthenticated = token存在 && token未过期 -
页面刷新 → 尝试用 RefreshToken 刷新 AccessToken
-
RefreshToken 无效 → 自动登出
-
-
企业级特点
-
用户体验:不会因为 AccessToken 过期而突然登出
-
安全性:AccessToken 短期有效,盗用风险低
-
全局统一:Axios 拦截器 + 后端校验 + 前端状态一致
-
| 情况 | 前端 UI | 后端验证 |
|---|---|---|
| AccessToken 有效 | 显示登录 | 接口正常返回数据 |
| AccessToken 过期,RefreshToken 有效 | 自动刷新 token → 显示登录 | 返回新 AccessToken |
| AccessToken 过期,RefreshToken 过期 | 前端 logout → 显示未登录 | 401 Unauthorized |
核心:AccessToken 到期不等于登出,只有 RefreshToken 到期才真正登出。
[用户访问页面 / 刷新页面]
│
▼
[AuthProvider useEffect 初始化]
│
▼
localStorage 检查 token / refreshToken / user
│
┌──────┴──────┐
│ │
token / refreshToken 不存在 → logout → 显示未登录
│
▼
token / refreshToken 存在 → 尝试刷新 token
│
┌──────┴──────┐
│ │
刷新成功 → 更新 AccessToken → 保持登录状态
刷新失败 → logout → 显示未登录
│
▼
[用户访问接口]
│
▼
后端验证 AccessToken
┌─────────┴─────────┐
│ │
AccessToken 有效 → 返回数据
AccessToken 过期 → 返回 401
│
▼
[Axios 拦截器捕获 401]
│
┌──────┴──────┐
│ │
尝试刷新 token RefreshToken 已过期 → logout
│
刷新成功 → 更新 AccessToken → 重试请求 → 用户无感知
刷新失败 → logout → 显示未登录
🔹 说明
-
前端缓存(localStorage)
-
只是"假设登录状态",不保证有效
-
主要作用:刷新页面时可以尝试恢复登录
-
-
AccessToken(短期)
-
用于每次接口访问
-
过期 → 后端返回 401 → 前端自动刷新或登出
-
-
RefreshToken(长期)
-
用于刷新 AccessToken
-
过期 → 必须登出,UI 才显示未登录
-
-
企业级特点
-
用户体验好:AccessToken 过期不影响 UI
-
安全性高:AccessToken 短期有效,盗用风险低
-
前端状态与后端权限同步,保证不会假象登录
前端 后端
─────────────── ───────────────用户登录 ───────────────▶ 验证账号密码
│
▼
返回 AccessToken(30m)
返回 RefreshToken(7d)
│
└─────────▶ 前端存 localStorage
token + refreshToken + user────────────────────────────────────────────────────────────
用户访问接口 ─────────────▶ 后端验证 AccessToken
├─ AccessToken 有效 → 返回数据
└─ AccessToken 过期 → 返回 401────────────────────────────────────────────────────────────
Axios 拦截器捕获 401 ─────────▶ 尝试刷新 AccessToken
│
┌────────────┴────────────┐
│ │
RefreshToken 有效 RefreshToken 过期
│ │
后端返回新 AccessToken 后端返回 401
│ │
前端更新 token → 重试原请求 前端 logout → 清理 localStorage
│ │
▼ ▼
用户无感知,继续操作 用户真正登出,UI显示未登录 -