Kotlin中Apollo拦截器GraphQL Refresh Token并发问题求助
问题分析与修复方案
核心问题点
- 并发状态不一致:多次拆分
mutex.withLock导致token读取、过期判断、刷新操作无法原子执行,竞态下会出现多个请求同时进入刷新逻辑,或拿到过期token。 - 请求头添加时机错误:在
chain.proceed()之后才添加version、build等头,这些头根本不会被发送到服务器。 - 刷新后请求逻辑错误:刷新token后直接用
refreshToken构建请求发送,而不是用新获取的accessToken重新发起原请求。 - 刷新状态管理失效:
isRefreshTokenInProgress的判断与修改未在同一锁内,并发下会出现多个请求同时触发刷新。
修复后的代码
class ApolloInterceptor @Inject constructor( private val loginViewModel: Lazy<LoginViewModel>, private val context: Context, private var storage: Storage, ) : HttpInterceptor { private val mutex = Mutex() private var isRefreshTokenInProgress = false override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { return mutex.withLock { // 原子性获取所有token相关信息 val currentToken = storage.getString(SharedPreferencesStorage.TOKEN) val currentRefreshToken = storage.getString(SharedPreferencesStorage.REFRESH_TOKEN) val tokenExpirationDate = storage.getString(SharedPreferencesStorage.TOKEN_EXPIRATION_DATE) // 处理过期判断(空值兜底) val isTokenExpired = if (tokenExpirationDate.isNullOrEmpty()) { true } else { val tokenExpirationUtc = ZonedDateTime.parse(tokenExpirationDate) .withZoneSameInstant(ZoneId.of("UTC")) val currentUtc = ZonedDateTime.now(ZoneId.of("UTC")) currentUtc.isAfter(tokenExpirationUtc) } // 需要刷新token的情况 if (isTokenExpired) { if (!isRefreshTokenInProgress) { isRefreshTokenInProgress = true try { // 等待刷新token完成(假设refreshToken是挂起函数,返回新token信息) loginViewModel.get().refreshToken().await() } finally { isRefreshTokenInProgress = false } } else { // 已有刷新请求在进行,等待锁释放后自动用新token重试 return@withLock intercept(request, chain) } // 刷新完成后,获取新token重新发起原请求 val newToken = storage.getString(SharedPreferencesStorage.TOKEN) return buildAndProceedRequest(request, chain, newToken) } else { // token有效,直接用当前token发送请求 return buildAndProceedRequest(request, chain, currentToken) } } } /** * 统一构建请求并发送,确保所有请求头都被正确添加 */ private suspend fun buildAndProceedRequest( request: HttpRequest, chain: HttpInterceptorChain, token: String? ): HttpResponse { val requestBuilder = request.newBuilder() .apply { token?.let { addHeader("Authorization", "Bearer $it") } addHeader("version", BuildConfig.VERSION_NAME) addHeader("build", BuildConfig.VERSION_CODE.toString()) addHeader("User-Agent", getDefaultUserAgent()) addHeader("language", getDeviceLanguage()) addHeader("theme", if (isDarkModeActive(context)) "dark" else "light") } return chain.proceed(requestBuilder.build()) } }
关键修复说明
- 原子性操作:将整个拦截逻辑包裹在
mutex.withLock中,确保token读取、过期判断、刷新操作的原子性,彻底解决并发竞态问题。 - 请求头统一处理:抽出
buildAndProceedRequest方法,在发送前统一添加所有请求头,避免之前的时机错误。 - 正确的刷新流程:
- 刷新token时标记
isRefreshTokenInProgress,并用try-finally确保状态重置,避免死锁。 - 已有刷新请求时,递归调用
intercept,等待锁释放后自动用新token重试。 - 刷新完成后重新获取新token,用新token构建原请求发送,实现自动重试。
- 刷新token时标记
- 日期处理优化:直接用
ZonedDateTime.now(ZoneId.of("UTC"))获取当前UTC时间,避免时区转换错误;增加空值兜底,防止过期日期为空时崩溃。
内容的提问来源于stack exchange,提问作者Achref Meghirbi
相关产品推荐
相关产品推荐

