并行API触发401时如何避免重复刷新Token并实现同步?
解决并行请求下重复刷新Token的问题
这个问题在并发REST请求场景下非常典型——多个请求同时触发401,导致重复发起Token刷新,既浪费资源还可能引发后端的刷新频率限制。核心思路是让刷新操作互斥,同时让等待的请求共享刷新后的Token结果,下面给你具体的实现方案:
1. 基于互斥锁的同步方案(适合Java等后端场景)
在Interceptor中维护一个全局的互斥锁,确保同一时间只有一个请求能执行刷新Token的逻辑,其他请求等待锁释放后直接复用新Token。
关键实现要点:
- 用全局锁控制刷新入口
- 加锁后二次检查Token状态(避免等待期间Token已被更新)
- 用
try-finally确保锁一定会释放
示例代码(Spring Boot拦截器场景):
import java.io.IOException; import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; import okhttp3.Request; import okhttp3.Response; import okhttp3.Interceptor; public class AuthInterceptor implements Interceptor { // 全局互斥锁,确保单例(可通过Spring @Bean注入) private final ReentrantLock refreshLock = new ReentrantLock(); // 用volatile保证多线程下Token的可见性 private volatile String currentToken; // 假设从存储获取Token的方法 private String getTokenFromStorage() { /* ... */ return currentToken; } private void saveTokenToStorage(String token) { /* ... */ currentToken = token; } // 刷新Token的核心方法 private String refreshToken() throws IOException { /* 调用刷新接口获取新Token */ } @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); // 先携带当前Token发起请求 Response response = proceedWithToken(originalRequest, currentToken); if (response.code() == 401) { refreshLock.lock(); try { // 二次检查:防止等待期间已有其他请求完成了Token刷新 String latestToken = getTokenFromStorage(); if (!Objects.equals(currentToken, latestToken)) { currentToken = latestToken; // 用最新Token重试请求 return proceedWithToken(originalRequest, currentToken); } // 执行Token刷新逻辑 String newToken = refreshToken(); currentToken = newToken; saveTokenToStorage(newToken); // 用新Token重试当前请求 return proceedWithToken(originalRequest, newToken); } finally { // 无论刷新成功失败,都释放锁 refreshLock.unlock(); } } return response; } // 封装携带Token发起请求的逻辑 private Response proceedWithToken(Request request, String token) throws IOException { Request authRequest = request.newBuilder() .header("Authorization", "Bearer " + token) .build(); return chain.proceed(authRequest); } }
2. 基于异步结果共享的方案(适合前端或异步后端场景)
如果是JavaScript前端或者用CompletableFuture的异步后端,可以通过维护一个全局的"刷新Promise/Future"来共享刷新结果,避免重复发起刷新。
关键实现要点:
- 全局变量存储正在进行的刷新Promise
- 刷新完成后清空Promise,允许下次触发
- 所有等待的请求共享同一个刷新结果
示例代码(JavaScript前端拦截器场景):
// 全局变量:存储正在进行的刷新Promise,初始为null let refreshPromise = null; // 当前Token,假设从localStorage获取 let currentToken = localStorage.getItem("token"); async function authInterceptor(requestConfig) { // 先携带当前Token发起请求 let response = await fetch(requestConfig.url, { ...requestConfig, headers: { ...requestConfig.headers, Authorization: `Bearer ${currentToken}` } }); if (response.status === 401) { // 检查是否已有正在进行的刷新操作 if (!refreshPromise) { // 发起刷新Token请求,封装为Promise refreshPromise = fetch("/api/refresh-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: localStorage.getItem("refreshToken") }) }) .then(res => res.json()) .then(data => { // 更新Token并存储 currentToken = data.accessToken; localStorage.setItem("token", currentToken); return currentToken; }) .catch(err => { // 刷新失败:清空Promise,抛出错误让上层处理 refreshPromise = null; throw new Error("Token刷新失败"); }) .finally(() => { // 无论成功失败,都清空刷新Promise refreshPromise = null; }); } // 等待刷新完成,获取新Token const newToken = await refreshPromise; // 用新Token重试请求 return fetch(requestConfig.url, { ...requestConfig, headers: { ...requestConfig.headers, Authorization: `Bearer ${newToken}` } }); } return response; }
3. 额外注意事项
- 避免无限循环:刷新Token后重试如果还是返回401,大概率是权限不足或Refresh Token过期,此时应该直接返回错误,不要继续重试。
- 锁的全局唯一性:确保锁或刷新Promise是全局单例,比如后端用Spring的单例Bean,前端用模块级变量。
- 异常处理:刷新Token失败时,一定要正确释放锁或清空Promise,避免后续请求一直处于等待状态。
内容的提问来源于stack exchange,提问作者manishkummar
相关产品推荐
相关产品推荐

