普通网友 2025-12-03 18:25 采纳率: 98.7%
浏览 0
已采纳

Kotlin中Token过期如何拦截并跳转登录?

在Kotlin开发的Android应用中,使用Retrofit配合OkHttp进行网络请求时,如何通过拦截器统一处理Token过期(如服务端返回401状态码),并自动跳转至登录页面?常见问题包括:拦截器中无法直接启动Activity、多次请求同时触发重复跳转、以及如何避免Token刷新逻辑的循环调用。如何优雅地实现全局拦截与登录页跳转?
  • 写回答

1条回答 默认 最新

  • 蔡恩泽 2025-12-03 18:29
    关注

    一、基础概念:OkHttp拦截器与Retrofit集成机制

    在Kotlin开发的Android应用中,使用Retrofit进行网络请求时,其底层依赖于OkHttp。因此,我们可以通过OkHttp提供的Interceptor机制,在请求发出前或响应返回后统一处理逻辑。

    Token过期通常由服务端返回401状态码标识。通过自定义拦截器,我们可以监听所有响应结果,判断是否为401错误,从而触发统一处理流程。

    val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(TokenRefreshInterceptor(context))
        .build()

    然而,直接在拦截器中启动Activity会引发问题——拦截器运行在OkHttp的IO线程中,并且不持有Activity引用,无法直接调用startActivity()

    此外,多个并发请求可能同时收到401响应,导致重复跳转登录页,造成用户体验混乱。

    更复杂的是,若实现自动刷新Token的逻辑,需防止因刷新失败再次触发401而导致的无限循环调用。

    这些问题促使我们需要一个分层、解耦、可扩展的解决方案。

    以下将从拦截器设计入手,逐步深入到事件分发与状态管理层面。

    二、常见问题分析与挑战

    • 无法直接启动Activity:拦截器处于网络层,不应持有UI组件引用,违反关注点分离原则。
    • 多次请求并发触发401跳转:多个API请求几乎同时返回401,导致多次弹出登录页或重复导航。
    • Token刷新逻辑嵌套导致死循环:刷新Token的请求本身也可能返回401,若未做特殊处理,会再次进入拦截器逻辑。
    • 上下文(Context)传递风险:持有Activity Context可能导致内存泄漏。
    • 测试困难:硬编码跳转逻辑使单元测试和Mock难以实施。

    这些问题表明,单纯的“在拦截器里startActivity”是不可取的反模式。

    三、分层架构设计:从拦截器到事件通知

    为解决上述问题,应采用职责分离的设计思想:

    层级职责技术实现
    网络层拦截响应、识别401、尝试刷新TokenOkHttp Interceptor + 同步锁
    业务逻辑层管理Token刷新流程、避免重复请求Suspend函数 + Mutex
    通知层告知UI层需要跳转登录LiveData / Flow / EventBus
    UI层接收通知并执行页面跳转Activity/Fragment观察者

    四、核心代码实现:带锁的Token刷新拦截器

    class TokenRefreshInterceptor(
        private val tokenManager: TokenManager,
        private val authService: AuthService
    ) : Interceptor {
    
        private val mutex = Mutex() // 防止并发刷新
    
        override suspend fun intercept(chain: Interceptor.Chain): Response {
            val originalRequest = chain.request()
            var response = chain.proceed(originalRequest)
    
            if (response.code == 401 && !originalRequest.url.toString().contains("refresh")) {
                // 尝试刷新Token
                val newToken = mutex.withLock {
                    if (tokenManager.shouldRefresh()) {
                        try {
                            val refreshResponse = authService.refreshToken(tokenManager.getRefreshToken())
                            if (refreshResponse.isSuccessful) {
                                val body = refreshResponse.body()
                                tokenManager.saveTokens(body!!.accessToken, body.refreshToken)
                                body.accessToken
                            } else {
                                null
                            }
                        } catch (e: Exception) {
                            null
                        }
                    } else {
                        null
                    }
                }
    
                if (newToken != null) {
                    // 使用新Token重试原请求
                    val newRequest = originalRequest.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                    response.close()
                    return chain.proceed(newRequest)
                } else {
                    // 刷新失败,标记已登出
                    tokenManager.clearTokens()
                    // 发送登出事件
                    LogoutEventBus.postLogout()
                }
            }
    
            return response
        }
    }

    五、事件驱动跳转:避免直接依赖Activity

    通过定义全局事件总线(如基于Kotlin Flow的单例),实现跨层通信:

    object LogoutEventBus {
        private val _logoutFlow = MutableSharedFlow(replay = 0)
        val logoutFlow: SharedFlow<Unit> = _logoutFlow.asSharedFlow()
    
        suspend fun postLogout() {
            _logoutFlow.emit(Unit)
        }
    }

    在BaseActivity中监听该事件:

    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            LogoutEventBus.logoutFlow.collect {
                startActivity(LoginActivity.newIntent(this@BaseActivity))
                finishAffinity()
            }
        }
    }

    六、流程图:Token过期处理完整流程

    graph TD A[发起网络请求] --> B{响应码是否为401?} B -- 否 --> C[正常返回数据] B -- 是 --> D{是否为refresh接口?} D -- 是 --> E[返回登录页] D -- 否 --> F[尝试获取锁] F -- 获取成功 --> G[调用刷新Token接口] G -- 成功 --> H[保存新Token, 重试原请求] G -- 失败 --> I[清除本地Token] I --> J[发送登出事件] J --> K[UI层监听并跳转登录页]

    七、优化策略与最佳实践

    1. 使用Mutex而非synchronized,适配Kotlin协程环境。
    2. 将AuthService注入拦截器,便于Mock测试。
    3. 对refresh请求单独配置Client,避免被同一拦截器反复拦截。
    4. 在Application级别注册全局事件监听,确保生命周期可控。
    5. 结合WorkManager处理离线Token刷新场景。
    6. 添加日志埋点,监控401发生频率与刷新成功率。
    7. 使用sealed class封装认证状态,提升类型安全。
    8. 考虑多账户切换下的Token隔离机制。
    9. 对敏感操作增加二次验证(如重新登录)。
    10. 利用DI框架(如Hilt)管理TokenManager生命周期。
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

问题事件

  • 已采纳回答 12月4日
  • 创建了问题 12月3日