在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、尝试刷新Token OkHttp 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层监听并跳转登录页]七、优化策略与最佳实践
- 使用Mutex而非synchronized,适配Kotlin协程环境。
- 将AuthService注入拦截器,便于Mock测试。
- 对refresh请求单独配置Client,避免被同一拦截器反复拦截。
- 在Application级别注册全局事件监听,确保生命周期可控。
- 结合WorkManager处理离线Token刷新场景。
- 添加日志埋点,监控401发生频率与刷新成功率。
- 使用sealed class封装认证状态,提升类型安全。
- 考虑多账户切换下的Token隔离机制。
- 对敏感操作增加二次验证(如重新登录)。
- 利用DI框架(如Hilt)管理TokenManager生命周期。
本回答被题主选为最佳回答 , 对您是否有帮助呢?解决 无用评论 打赏 举报