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,提问作者Нурик Курбанов
相关产品推荐
相关产品推荐

