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
相关产品推荐
相关产品推荐

