无感刷新 Token 需要前后端配合实现
流程如下
- 用户登录成功后,接口返回
accessToken和refrshToken。前端将这两个token存在本地。 - 后续业务接口请求时,需要在
请求头上加上accessToken - 当accessToken过期时,调接口(带上refreshToken参数)重新获取 accessToken,然后将失败的、暂停的请求
带上最新的accessToken重新处理一遍(队列管理这些请求) - 如果刷新 Token 失败,则跳转到登录页重新登录
伪代码如下
js
import axios from 'axios';
// 创建一个新的axios实例
const api = axios.create({
baseURL: '/api',
timeout: 5000,
});
// ------------------- 请求拦截器 -------------------
api.interceptors.request.use(config => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// ------------------- 响应拦截器 -------------------
// 用于标记是否正在刷新token
let isRefreshing = false;
// 用于存储因为token过期而被挂起的请求
let requestsQueue = [];
api.interceptors.response.use(
response => {
return response;
},
async error => {
const { config, response } = error;
// 如果返回的HTTP状态码是401,说明access_token过期了
if (response && response.status === 401) {
// 如果当前没有在刷新token,那么我们就去刷新token
if (!isRefreshing) {
isRefreshing = true;
try {
// 调用刷新token的接口
const { data } = await axios.post('/refresh-token', {
refreshToken: localStorage.getItem('refreshToken')
});
const newAccessToken = data.accessToken;
localStorage.setItem('accessToken', newAccessToken);
// token刷新成功后,重新执行所有被挂起的请求
requestsQueue.forEach(cb => cb(newAccessToken));
// 清空队列
requestsQueue = [];
// 把本次失败的请求也重新执行一次
config.headers.Authorization = `Bearer ${newAccessToken}`;
return api(config);
} catch (refreshError) {
// 如果刷新token也失败了,说明refreshToken也过期了
// 此时只能清空本地存储,跳转到登录页
console.error('Refresh token failed:', refreshError);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
} else {
// 如果当前正在刷新token,就把这次失败的请求,存储到队列里
// 返回一个pending的Promise,等token刷新后再去执行
return new Promise((resolve) => {
requestsQueue.push((newAccessToken) => {
config.headers.Authorization = `Bearer ${newAccessToken}`;
resolve(api(config));
});
});
}
}
return Promise.reject(error);
}
);
export default api;
代码里面有两个地方需要注意:
-
isRefreshing 状态锁:
这是为了解决并发问题。想象一下,如果一个页面同时发起了多个API请求,而accessToken刚好过期,多个请求会同时收到401。如果没有isRefreshing这个锁,它们会同时去调用/refresh-token接口,发起多次刷新请求,这是完全没有必要的浪费,甚至可能因为并发问题导致后端逻辑出错。
有了这个锁,只有第一个收到401的请求,会真正去执行刷新逻辑。
-
requestsQueue 请求队列:
比如现在有3个请求,第1个请求正在刷新Token(isRefreshing = true),后面那2个请求把它们的resolve函数推进一个队列(requestsQueue)里,暂时挂起。
等第一个请求成功拿到新的accessToken后,再遍历这个队列,把所有被挂起的请求,用新的Token重新执行一遍。