JWT刷新令牌应该如何设计才能避免多次请求时的重复验证?

程序猿淑芳 阅读 69

我在做文件上传功能时发现,当用户同时上传多个文件时,每个文件请求都会带着JWT访问接口,但遇到令牌过期的情况,所有请求都会触发刷新逻辑。比如这样:


axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 401 && !error.config.sentRefresh) {
      return refreshToken().then(newToken => {
        error.config.headers.Authorization = Bearer ${newToken};
        error.config.sentRefresh = true;
        return axios(error.config);
      });
    }
    return Promise.reject(error);
  }
);

虽然加了sentRefresh标记避免重复刷新,但实际测试时发现:当第一个请求触发刷新时,第二个请求可能在第一个刷新完成前也进入拦截器,导致重复发起刷新请求。有没有更好的方式让多个并发请求共享同一个刷新流程?

另外发现如果用户同时打开多个标签页上传文件,不同标签页的刷新请求会覆盖彼此的token,这样其他标签页的请求又会失效,这该怎么处理?

我来解答 赞 11 收藏
二维码
手机扫码查看
2 条解答
慕容法霞
这个问题确实有点棘手,涉及到并发请求和多个标签页之间的状态管理。我们可以从两方面入手解决:一是确保并发请求共享同一个刷新流程;二是避免不同标签页之间的token覆盖问题。

首先,对于并发请求的刷新问题,我们可以引入一个全局的Promise来表示当前的刷新操作。这样当有多个请求同时触发刷新时,它们可以等待同一个Promise的结果,而不是各自发起刷新请求。

然后,针对不同标签页的问题,我们可以利用浏览器的本地存储机制,比如localStorage或sessionStorage,来同步各个标签页的状态。这里我们可以使用Broadcast Channel API来实现跨标签页的消息通信,从而确保所有标签页都能及时更新token。

下面是一个简单的实现示例:

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});

failedQueue = [];
};

axios.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;

if (error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = Bearer ${token};
return axios(originalRequest);
}).catch(err => {
return Promise.reject(err);
});
}

originalRequest._retry = true;
isRefreshing = true;

return refreshToken().then(newToken => {
originalRequest.headers.Authorization = Bearer ${newToken};
processQueue(null, newToken);
return axios(originalRequest);
}).catch(refreshError => {
processQueue(refreshError, null);
return Promise.reject(refreshError);
}).finally(() => {
isRefreshing = false;
});
}

return Promise.reject(error);
}
);

// 使用Broadcast Channel API同步token
const broadcastChannel = new BroadcastChannel('auth-channel');

broadcastChannel.onmessage = event => {
if (event.data.type === 'tokenUpdated') {
localStorage.setItem('accessToken', event.data.token);
processQueue(null, event.data.token);
}
};

function refreshToken() {
// 假设这是你的刷新token逻辑
return axios.post('/refresh-token').then(response => {
const newToken = response.data.accessToken;
localStorage.setItem('accessToken', newToken);
broadcastChannel.postMessage({ type: 'tokenUpdated', token: newToken });
return newToken;
});
}


这段代码中,我们通过failedQueue来存储所有因token过期而失败的请求,并在刷新成功后重新发送这些请求。同时,使用Broadcast Channel API来通知所有标签页更新token,确保一致性。这样应该能有效解决你提到的问题。
点赞
2026-03-22 22:02
梓熙的笔记
用一个 Promise 变量挂起刷新过程,所有并发请求都等待这个 Promise 完成。localStorage 监听解决多标签覆盖问题,改成这样:

let isRefreshing = null;
let subscribers = [];

axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401 && !error.config.sentRefresh) {
if (!isRefreshing) {
isRefreshing = refreshToken();
isRefreshing.then(() => {
subscribers.forEach(cb => cb());
subscribers = [];
isRefreshing = null;
});
}

error.config.sentRefresh = true;
return new Promise(resolve => {
subscribers.push(() => {
error.config.headers.Authorization = Bearer ${localStorage.getItem('token')};
resolve(axios(error.config));
});
});
}
return Promise.reject(error);
}
);

window.addEventListener('storage', e => {
if (e.key === 'token') {
localStorage.setItem('token', e.newValue);
}
});
点赞 10
2026-02-06 14:04