OAuth2.0静默刷新时怎么避免重复发送refresh_token请求?
我在做前端静默刷新访问令牌时遇到个问题,当用户快速切换页面时,多个定时器同时触发导致重复发送了refresh_token请求。比如这样写的:
let refreshTimeout;
function startRefresh() {
refreshTimeout = setTimeout(() => {
axios.post('/api/refresh', { refresh_token })
.then(res => localStorage.setItem('access_token', res.data.token));
}, 350000);
}
后来尝试用防抖函数改成了:
function debounceRefresh(func, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
但后端还是返回了”refresh_token已被使用”的错误,看起来多个请求还是在同时到达。有没有更好的方式让静默刷新既可靠又不会重复请求?
我的做法是用一个 pending 状态来标记是否已经在刷新,如果已经有请求在跑了,后续的触发就直接跳过。代码大概是这样的:
let isRefreshing = false;
let refreshPromise = null;
function refreshToken() {
if (isRefreshing) {
return refreshPromise;
}
isRefreshing = true;
refreshPromise = new Promise((resolve, reject) => {
axios.post('/api/refresh', { refresh_token })
.then(res => {
localStorage.setItem('access_token', res.data.token);
resolve(res.data.token);
})
.catch(err => {
reject(err);
})
.finally(() => {
isRefreshing = false;
refreshPromise = null;
});
});
return refreshPromise;
}
然后你 startRefresh 的时候用这个函数就行。这个方案的好处是,不管定时器怎么触发,真正的刷新逻辑只会执行一次。其他调用都会复用同一个 Promise。
另外建议在拦截器里面做 token 的刷新管理,而不是用定时器。比如判断 token 过期时间,或者在每次请求失败的时候尝试刷新 token。这样更符合实际请求需求,也更容易做并发控制。
你得加一个「刷新中」的状态锁,比如:
let isRefreshing = false;
let refreshPromise = null;
function refreshToken() {
if (isRefreshing) {
return refreshPromise;
}
isRefreshing = true;
refreshPromise = axios.post('/api/refresh', { refresh_token })
.then(res => {
const newToken = res.data.token;
localStorage.setItem('access_token', newToken);
return newToken;
})
.finally(() => {
isRefreshing = false;
});
return refreshPromise;
}
这样不管多少个地方同时调用 refreshToken,都只会发一次请求,后面的都等同一个 promise 返回。
另外,定时器那边可以继续用 debounce 或 setTimeout 控制触发时机,但核心得靠这个状态锁来保证只有一个刷新过程在进行。
我当时也踩过这坑,前端静默刷新一定要加这个状态锁,不然 refresh_token 很容易被后端标记为重复使用。