You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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())
    }
}

关键修复说明

  1. 原子性操作:将整个拦截逻辑包裹在mutex.withLock中,确保token读取、过期判断、刷新操作的原子性,彻底解决并发竞态问题。
  2. 请求头统一处理:抽出buildAndProceedRequest方法,在发送前统一添加所有请求头,避免之前的时机错误。
  3. 正确的刷新流程:
    • 刷新token时标记isRefreshTokenInProgress,并用try-finally确保状态重置,避免死锁。
    • 已有刷新请求时,递归调用intercept,等待锁释放后自动用新token重试。
    • 刷新完成后重新获取新token,用新token构建原请求发送,实现自动重试。
  4. 日期处理优化:直接用ZonedDateTime.now(ZoneId.of("UTC"))获取当前UTC时间,避免时区转换错误;增加空值兜底,防止过期日期为空时崩溃。

内容的提问来源于stack exchange,提问作者Achref Meghirbi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 18:52:42