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

Android中Retrofit Interceptor多请求场景下的Token刷新问题

解决Retrofit AuthInterceptor并发刷新Token重复请求问题

问题说明

在Android应用中使用Retrofit的AuthInterceptor实现Token自动刷新功能时,单请求场景运行正常,但当Token过期且存在多请求并发时,每个请求都会独立触发/v1/user/refresh接口调用,导致重复刷新Token,最终引发Token失效问题。

原问题代码

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()

        val userIsNotAuthorize = runBlocking { !authorizationRepository.userIsAuthorize().first() }

        if (userIsNotAuthorize) {
            return chain.proceed(originalRequest)
        }

        val accessToken = authorizationRepository.getAccessToken()

        val tokenHasExpired = authorizationRepository.tokenHasExpired()

        return if (tokenHasExpired) {
            chain.proceedDeletingTokenOnError(
                originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build()
            )
        } else {
            val refreshTokenRequest = originalRequest.newBuilder()
                .post("".toRequestBody("application/json".toMediaType()))
                .url(BuildConfig.BASE_URL + "/v1/user/refresh")
                .addHeaders(accessToken)
                .build()

            val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)

            refreshResponse.use {
                if (it.isSuccessful) {
                    val refreshedToken = Gson().fromJson(
                        it.body?.string(),
                        AuthorizedUserDto::class.java
                    )
                    authorizationRepository.setAccessToken(
                        refreshedToken.items.token,
                        refreshedToken.items.expiredAt
                    )
                    val newCall = originalRequest.newBuilder()
                        .addHeaders(refreshedToken.items.token)
                        .build()
                    chain.proceedDeletingTokenOnError(newCall)
                } else {
                    chain.proceedDeletingTokenOnError(originalRequest)
                }
            }
        }
    }

    private inline fun Request.Builder.addHeaders(token: String) =
        this.apply { header("Authorization", "Bearer $token") }

    private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
        val response = proceed(request)
        if (response.code == 401) {
            runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
        }
        return response
    }
}

解决方案:加锁控制刷新流程

核心思路是通过互斥锁保证同一时间只有一个请求执行Token刷新操作,其他请求等待刷新完成后直接使用新Token重试。

修改后的代码

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {
    // 信号量实现互斥锁,同一时间仅允许一个请求进入刷新流程
    private val refreshLock = Semaphore(1)

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()

        val userIsAuthorized = runBlocking { authorizationRepository.userIsAuthorize().first() }
        if (!userIsAuthorized) {
            return chain.proceed(originalRequest)
        }

        var accessToken = authorizationRepository.getAccessToken()
        var tokenHasExpired = authorizationRepository.tokenHasExpired()

        // Token未过期,直接携带Token发起请求
        if (!tokenHasExpired) {
            return chain.proceedDeletingTokenOnError(
                originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build()
            )
        }

        // Token过期,进入刷新流程
        return try {
            // 获取锁,阻塞其他请求直到当前刷新完成
            refreshLock.acquire()

            // 再次检查Token状态:避免等待锁期间已被其他请求刷新
            accessToken = authorizationRepository.getAccessToken()
            tokenHasExpired = authorizationRepository.tokenHasExpired()

            if (!tokenHasExpired) {
                // 已有新Token,直接用新Token重试原请求
                chain.proceedDeletingTokenOnError(
                    originalRequest.newBuilder()
                        .addHeaders(accessToken)
                        .build()
                )
            } else {
                // 执行Token刷新请求
                val refreshResponse = chain.proceedDeletingTokenOnError(
                    originalRequest.newBuilder()
                        .post("".toRequestBody("application/json".toMediaType()))
                        .url(BuildConfig.BASE_URL + "/v1/user/refresh")
                        .addHeaders(accessToken)
                        .build()
                )

                refreshResponse.use {
                    if (it.isSuccessful) {
                        // 解析新Token并更新本地存储
                        val refreshedToken = Gson().fromJson(
                            it.body?.string(),
                            AuthorizedUserDto::class.java
                        )
                        authorizationRepository.setAccessToken(
                            refreshedToken.items.token,
                            refreshedToken.items.expiredAt
                        )
                        // 用新Token重试原请求
                        chain.proceedDeletingTokenOnError(
                            originalRequest.newBuilder()
                                .addHeaders(refreshedToken.items.token)
                                .build()
                        )
                    } else {
                        // 刷新失败,返回原请求错误(或触发未授权状态)
                        chain.proceedDeletingTokenOnError(originalRequest)
                    }
                }
            }
        } catch (e: InterruptedException) {
            // 处理中断异常,恢复线程中断状态
            Thread.currentThread().interrupt()
            chain.proceedDeletingTokenOnError(originalRequest)
        } finally {
            // 无论成功失败,都释放锁,让其他请求继续执行
            refreshLock.release()
        }
    }

    private inline fun Request.Builder.addHeaders(token: String) =
        this.apply { header("Authorization", "Bearer $token") }

    private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
        val response = proceed(request)
        if (response.code == 401) {
            runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
        }
        return response
    }
}

关键改进点

  • 互斥锁控制:使用Semaphore(1)实现全局互斥,确保同一时间只有一个请求执行Token刷新,避免重复调用刷新接口
  • 双重检查Token状态:获取锁后再次检查Token是否过期,防止等待锁期间已被其他请求完成刷新,减少无效操作
  • 安全释放锁:通过try-finally保证锁一定会被释放,避免死锁风险
  • 逻辑分支优化:调整代码顺序,让Token未过期的路径更直接,提升可读性

额外优化建议

  • 尽量避免在拦截器中使用runBlocking,可将authorizationRepository的相关方法改为挂起函数,配合withContext(Dispatchers.IO)处理协程逻辑,减少线程阻塞
  • 将Token刷新逻辑封装到authorizationRepository中,让拦截器仅负责请求拦截和重试,职责更单一
  • 增加刷新请求的异常捕获(如网络异常),避免因异常导致锁无法释放

内容的提问来源于stack exchange,提问作者Нурик Курбанов

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 05:02:02