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

Android Kotlin Retrofit实现RefreshToken刷新AccessToken异常排查

Android Kotlin+Retrofit Token自动续期异常排查与实现方案

现有代码异常原因

  • 异步逻辑时序错误:OkHttp的authenticate是同步阻塞回调,方法内部启动CoroutineScope.launch属于异步非阻塞执行,协程还未请求到新Token,方法就已经执行return,此时newAccess未初始化,要么直接抛出lateinit异常,要么拿到空值拼到请求头里。
  • 依赖未初始化:loginRepository、bodyRefresh、access三个lateinit变量只做了声明,没有实际赋值,运行时会直接抛出未初始化异常。
  • Flow使用错误:getUserRefresh()返回的是Flow,调用collect会持续订阅数据变化,不是一次性取值,会导致刷新逻辑反复触发。
  • 存储逻辑错误:刷新接口返回新Access Token时,错误调用saveUserRefresh方法,会把原有有效的Refresh Token覆盖成Access Token,导致后续刷新请求失败。
  • 缺少防死循环逻辑:如果Refresh Token本身过期也返回401,当前逻辑会无限触发刷新接口重试,直接把应用打崩。
  • 字符串拼接错误:请求头拼接时写"Bearer $newAccess.toString()",会多余调用一次toString,且newAccess为空时会直接把null拼到请求头里。
  • 协程使用不规范:自行创建的CoroutineScope没有绑定生命周期,容易造成内存泄漏,且在网络请求场景不该用Main调度器切主线程。

正确实现方案

authenticate方法本身运行在OkHttp的后台线程,不需要额外启动异步协程,直接用runBlocking切换到IO线程执行挂起函数、等待刷新结果即可。

1. 实现带并发锁、防死循环的Authenticator

class RefreshTokenAuthenticator(
    private val tokenDataStore: StoreAccess,
    private val apiService: ApiService
) : Authenticator {
    // 并发锁,避免多请求同时触发401时重复调用刷新接口
    private val refreshLock = Any()
    @Volatile
    private var isRefreshing = false

    override fun authenticate(route: Route?, response: Response): Request? {
        // 非401错误不处理,直接返回原请求
        if (response.code != 401) return response.request
        // 已经重试过的请求仍返回401,说明RefreshToken已过期,终止请求触发登录跳转
        if (response.request.header("Authorization-Retry") == "true") {
            // 此处可发送全局事件跳转登录页,例如通过Flow、LiveData发通知
            return null
        }

        synchronized(refreshLock) {
            // 先拿当前本地存储的最新AccessToken
            val localAccessToken = runBlocking(Dispatchers.IO) {
                tokenDataStore.getUserAccess().first() // 用first()一次性取值,不要collect持续订阅
            }
            // 如果当前请求带的token和本地最新token不一致,说明其他请求已经刷新过token,直接用新token重试
            val requestToken = response.request.header("Authorization")?.removePrefix("Bearer ")?.trim()
            if (requestToken != localAccessToken && localAccessToken.isNotBlank()) {
                return response.request.newBuilder()
                    .header("Authorization", "Bearer $localAccessToken")
                    .header("Authorization-Retry", "true")
                    .build()
            }

            if (isRefreshing) return@synchronized null
            isRefreshing = true

            return try {
                // 读取本地存储的RefreshToken
                val refreshToken = runBlocking(Dispatchers.IO) {
                    tokenDataStore.getUserRefresh().first()
                }
                if (refreshToken.isBlank()) return null

                // 同步调用刷新接口
                val refreshResp = runBlocking(Dispatchers.IO) {
                    apiService.refreshAcssec(BodyRefresh(refresh = refreshToken))
                }
                if (!refreshResp.isSuccessful || refreshResp.body() == null) return null

                val newAccessToken = refreshResp.body()!!.access
                // 存储新的AccessToken,注意不要覆盖原有RefreshToken,除非接口返回了新的RefreshToken
                runBlocking(Dispatchers.IO) {
                    tokenDataStore.saveUserAccess(newAccessToken)
                    // refreshResp.body()!!.refresh?.let { tokenDataStore.saveUserRefresh(it) }
                }

                // 构建带新token的重试请求
                response.request.newBuilder()
                    .header("Authorization", "Bearer $newAccessToken")
                    .header("Authorization-Retry", "true")
                    .build()
            } catch (e: Exception) {
                e.printStackTrace()
                null
            } finally {
                isRefreshing = false
            }
        }
    }
}

2. 配置OkHttp客户端

val okHttpClient = OkHttpClient.Builder()
    // 先加普通请求的token拦截器
    .addInterceptor(AuthInterceptor(tokenDataStore))
    // 再加自动刷新的Authenticator
    .authenticator(RefreshTokenAuthenticator(tokenDataStore, apiService))
    .build()

3. 附普通请求Token拦截器参考实现

class AuthInterceptor(private val tokenDataStore: StoreAccess) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalReq = chain.request()
        val accessToken = runBlocking(Dispatchers.IO) {
            tokenDataStore.getUserAccess().first()
        }
        return if (accessToken.isNotBlank()) {
            val newReq = originalReq.newBuilder()
                .header("Authorization", "Bearer $accessToken")
                .build()
            chain.proceed(newReq)
        } else {
            chain.proceed(originalReq)
        }
    }
}

关键注意点

  • 所有依赖通过构造函数传入,不要用lateinit声明后不赋值,避免运行时崩溃
  • 从DataStore取单次值用Flow.first(),不要用collect持续订阅
  • 必须加重试标记,避免RefreshToken过期时触发无限重试
  • 刷新成功后不要随意覆盖原RefreshToken,除非接口明确返回了新的RefreshToken

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 18:09:21