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

如何在TokenAuthenticator刷新token失败时显示错误并跳转登录页?

问题根因

你遇到的两个核心问题根因非常明确:

  • 无限循环触发的原因有两点:
    • 当前authenticate方法中使用GlobalScope.launch异步执行刷新逻辑,方法直接返回null,OkHttp收到null返回值后会立即重试原请求,原请求携带旧Token依然会返回401,再次触发authenticate逻辑,形成死循环
    • 普通MutableLiveData是粘性事件,你没有对isRefreshTokenFail做消费后重置的处理,每次Activity生命周期回调时都会收到之前发送的true值,反复触发跳转逻辑
  • 刷新成功后新请求不生效的原因:你在协程里构建了新的带新Token的Request,但是没有作为方法返回值返回,实际不会生效

解决方案

步骤1:修复TokenAuthenticator核心逻辑

authenticate方法本身运行在OkHttp的后台线程,不会阻塞主线程,可以直接用runBlocking同步等待刷新结果,同时添加刷新中标记避免并发刷新:

class TokenAuthenticator(
    val preferenceHelperImp: PreferenceHelperImp,
    private val tokenApi: RefreshTokenApi,
) : Authenticator{
    // 加锁防止多个请求同时触发重复刷新
    private val refreshLock = Any()
    private var isRefreshing = false

    override fun authenticate(route: Route?, response: Response): Request? {
        // 已经在刷新中则直接返回null避免重复执行
        if (isRefreshing) return null
        synchronized(refreshLock) {
            if (isRefreshing) return null
            isRefreshing = true
        }

        return runBlocking {
            var newRequest: Request? = null
            getUpdatedRefreshToken(RefreshTokenRequest(preferenceHelperImp.getRefreshToken())
            ).collect {
                when (it) {
                    is State.Success -> {
                        preferenceHelperImp.setCurrentUserLoggedInMode(Constants.App.LoggedInMode.LOGGED_IN_MODE_SERVER)
                        val accessToken = it.data.body()?.payload?.accessToken ?: ""
                        val refreshToken = it.data.body()?.payload?.refreshToken ?: ""
                        preferenceHelperImp.setAccessToken(accessToken)
                        preferenceHelperImp.setRefreshToken(refreshToken)
                        preferenceHelperImp.setUserInfo(Gson().toJson(TokenInfo.initFromToken(accessToken)))

                        Session.current.userInfo =
                            Gson().fromJson(preferenceHelperImp.getUserInfo(),
                                TokenInfo::class.java)

                        // 构建新请求作为方法返回值,OkHttp会自动重试这个新请求
                        newRequest = response.request.newBuilder()
                            .header("Authorization", "Bearer $accessToken")
                            .build()
                    }
                    is State.Fail, is State.Error -> {
                        // 先清空本地存储的登录信息,避免后续请求重复触发刷新逻辑
                        preferenceHelperImp.clearLoginInfo()
                        Session.current.userInfo = null
                        // 发送刷新失败事件
                        Session.current.isRefreshTokenFail.postValue(true)
                        newRequest = null
                    }
                }
            }
            isRefreshing = false
            newRequest
        }
    }

    private fun getUpdatedRefreshToken(refreshTokenRequest: RefreshTokenRequest): Flow<State<LoginResponse>> {
        return object :
            NetworkBoundRepository<LoginResponse>() {
            override suspend fun fetchFromRemote(): retrofit2.Response<LoginResponse> =
                tokenApi.getRefreshToken(refreshTokenRequest)
        }.asFlow()
    }
}

步骤2:替换普通MutableLiveData为单次事件类

避免粘性事件重复触发,自定义只会消费一次的SingleLiveEvent:

class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }

    override fun setValue(t: T?) {
        pending.set(true)
        super.setValue(t)
    }

    override fun postValue(value: T) {
        pending.set(true)
        super.postValue(value)
    }
}

修改Session类中的属性定义:

val isRefreshTokenFail = SingleLiveEvent<Boolean>()

步骤3:BaseActivity中添加观察逻辑

不需要给TokenAuthenticator传Context,直接在BaseActivity统一处理跳转即可:

// 在onCreate生命周期中添加观察
Session.current.isRefreshTokenFail.observe(this) { isFail ->
    if (isFail) {
        // 展示错误提示
        Toast.makeText(this, "登录状态已过期,请重新登录", Toast.LENGTH_SHORT).show()
        // 跳转到登录页,清空任务栈避免回退
        val intent = Intent(this, LoginActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        startActivity(intent)
    }
}

可选方案:直接注入Application跳转

如果你不想用事件通知的方案,可以直接在Hilt模块中注入ApplicationContext给TokenAuthenticator,刷新失败时直接跳转:

// NetworkModule中修改提供逻辑
@Provides
fun provideTokenAuthenticator(
    preferenceHelperImp: PreferenceHelperImp,
    tokenApi: RefreshTokenApi,
    @ApplicationContext context: Context
): TokenAuthenticator {
    return TokenAuthenticator(preferenceHelperImp, tokenApi, context)
}

刷新失败时直接用Context跳转即可,注意要加FLAG_ACTIVITY_NEW_TASK标记。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 12:21:03