跳到主要内容

Token 无感刷新

场景

JWT access token 过期后需要用 refresh token 获取新的 access token,要求用户无感知,不中断操作。

完整实现

核心难点

  1. 请求返回 401 时自动刷新 token
  2. 并发请求:多个请求同时 401,只刷新一次
  3. 刷新成功后重放失败的请求
  4. refresh token 也过期则跳到登录

Axios 拦截器实现

无感刷新完整方案
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';

const api = axios.create({ baseURL: '/api' });

let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: Error) => void;
}> = [];

// 处理等待队列
function processQueue(error: Error | null, token: string | null) {
failedQueue.forEach((promise) => {
if (error) {
promise.reject(error);
} else {
promise.resolve(token!);
}
});
failedQueue = [];
}

// 请求拦截器:自动带上 token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// 响应拦截器:处理 401
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}

if (isRefreshing) {
// 已经在刷新中,排队等待
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(api(originalRequest));
},
reject,
});
});
}

originalRequest._retry = true;
isRefreshing = true;

try {
const refreshToken = localStorage.getItem('refresh_token');
const { data } = await axios.post('/api/auth/refresh', { refreshToken });

localStorage.setItem('access_token', data.accessToken);
localStorage.setItem('refresh_token', data.refreshToken);

// 通知排队的请求
processQueue(null, data.accessToken);

// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// refresh token 也过期了
processQueue(refreshError as Error, null);
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);

流程图


常见面试问题

Q1: 多个请求同时 401 怎么处理?

答案

用一个 isRefreshing 锁 + failedQueue 队列。第一个 401 触发刷新,后续 401 请求进入队列等待。刷新完成后拿到新 token,遍历队列重新发送所有等待的请求。

Q2: refresh token 也过期了怎么办?

答案

直接清除所有 token,跳转到登录页。同时清空等待队列中的所有请求,返回 reject。

Q3: access token 和 refresh token 存在哪里?

答案

存储位置access tokenrefresh token
内存变量✅ 最安全❌ 刷新丢失
localStorage✅ 常见⚠️ XSS 风险
httpOnly Cookie✅ 安全✅ 推荐

最安全的方案:access token 存内存,refresh token 存 httpOnly Cookie,由服务端自动携带。

相关链接