Android 拦截器与协程融合:实现无感 Token 预刷新方案

Android 拦截器与协程融合:实现无感 Token 预刷新方案

在Android应用开发中,Token认证是保障API安全的核心机制。然而,传统方案中Token过期导致的请求失败、用户频繁重登等问题,严重影响了用户体验。本文提出一种基于OkHttp拦截器与协程的无感知Token预刷新方案,通过拦截器捕获Token过期信号,结合协程的挂起机制与并发控制,实现Token的静默更新,确保业务请求的连续性。

一、方案核心设计思路

1.1 拦截器:Token状态的“守门人”

拦截器是方案的核心组件,负责在请求发出前检查Token有效性,并在响应阶段捕获401未授权错误。通过重写OkHttp的intercept方法,可实现以下逻辑:

  • 请求阶段:从本地存储(如SharedPreferences或加密数据库)读取当前Token,附加到请求头。
  • 响应阶段:若服务器返回401错误,拦截器需暂停当前请求,触发Token刷新流程,待新Token就绪后重试原请求。

1.2 协程:异步刷新的“调度器”

协程的轻量级线程特性与挂起函数(suspend)机制,使其成为处理异步刷新的理想选择。方案中需定义以下协程任务:

  • 刷新任务:调用Token刷新API,获取新Token并更新本地存储。
  • 重试任务:在刷新完成后,重新发起被拦截的原始请求。

1.3 并发控制:避免“刷新风暴”

当多个请求同时触发401错误时,需避免重复刷新Token。通过共享的Mutex锁或AtomicBoolean标志位,确保同一时间仅有一个刷新任务执行。

二、技术实现步骤

2.1 定义Token管理类

封装Token的存储、刷新与状态管理逻辑:

  1. class TokenManager(private val context: Context) {
  2. private val tokenStore = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
  3. private val refreshLock = Mutex() // 协程互斥锁
  4. suspend fun getToken(): String? = tokenStore.getString("token", null)
  5. suspend fun refreshToken(refreshToken: String): Result<String> = coroutineScope {
  6. refreshLock.withLock {
  7. // 检查是否已有刷新任务在执行
  8. if (isRefreshing.compareAndSet(false, true)) {
  9. try {
  10. val response = apiClient.refreshToken(refreshToken)
  11. tokenStore.edit().putString("token", response.newToken).apply()
  12. Result.success(response.newToken)
  13. } catch (e: Exception) {
  14. Result.failure(e)
  15. } finally {
  16. isRefreshing.set(false)
  17. }
  18. } else {
  19. // 等待其他任务完成
  20. delay(100) // 简单轮询,实际可用Channel优化
  21. getToken()?.let { Result.success(it) }
  22. ?: Result.failure(IllegalStateException("Token refresh failed"))
  23. }
  24. }
  25. }
  26. }

2.2 实现拦截器逻辑

自定义拦截器需处理三种场景:

  1. 正常请求:Token有效,直接放行。
  2. Token过期:触发刷新并重试。
  3. 刷新中:挂起当前请求,等待刷新完成。
  1. class TokenInterceptor(
  2. private val tokenManager: TokenManager,
  3. private val scope: CoroutineScope
  4. ) : Interceptor {
  5. override fun intercept(chain: Interceptor.Chain): Response {
  6. val originalRequest = chain.request()
  7. val token = runBlocking { tokenManager.getToken() }
  8. ?: return chain.proceed(originalRequest) // 无Token直接放行
  9. val newRequest = originalRequest.newBuilder()
  10. .header("Authorization", "Bearer $token")
  11. .build()
  12. val response = chain.proceed(newRequest)
  13. if (response.code == HTTP_UNAUTHORIZED) {
  14. response.close() // 关闭原响应
  15. return retryWithFreshToken(chain, originalRequest)
  16. }
  17. return response
  18. }
  19. private fun retryWithFreshToken(chain: Interceptor.Chain, originalRequest: Request): Response {
  20. val refreshToken = runBlocking { tokenManager.getRefreshToken() }
  21. ?: throw IllegalStateException("Refresh token missing")
  22. return scope.async {
  23. try {
  24. tokenManager.refreshToken(refreshToken)
  25. val newToken = runBlocking { tokenManager.getToken() }
  26. ?: throw IllegalStateException("Token refresh failed")
  27. val freshRequest = originalRequest.newBuilder()
  28. .header("Authorization", "Bearer $newToken")
  29. .build()
  30. chain.proceed(freshRequest)
  31. } catch (e: Exception) {
  32. chain.proceed(originalRequest) // 刷新失败时返回原始错误
  33. }
  34. }.await()
  35. }
  36. }

2.3 优化并发控制

上述代码中简单的轮询可能导致性能问题,可通过Channel实现更高效的等待机制:

  1. private suspend fun retryWithFreshToken(chain: Interceptor.Chain, originalRequest: Request): Response {
  2. val refreshToken = runBlocking { tokenManager.getRefreshToken() }
  3. ?: throw IllegalStateException("Refresh token missing")
  4. val refreshChannel = Channel<String>(Channel.RENDEZVOUS)
  5. scope.launch {
  6. try {
  7. tokenManager.refreshToken(refreshToken)
  8. val newToken = runBlocking { tokenManager.getToken() }
  9. ?: throw IllegalStateException("Token refresh failed")
  10. refreshChannel.send(newToken)
  11. } catch (e: Exception) {
  12. refreshChannel.close(e)
  13. }
  14. }
  15. return try {
  16. val newToken = refreshChannel.receive() // 挂起等待新Token
  17. val freshRequest = originalRequest.newBuilder()
  18. .header("Authorization", "Bearer $newToken")
  19. .build()
  20. chain.proceed(freshRequest)
  21. } catch (e: Exception) {
  22. chain.proceed(originalRequest) // 刷新失败时返回原始错误
  23. }
  24. }

三、最佳实践与注意事项

3.1 刷新令牌的有效期管理

  • 本地存储的Refresh Token需设置合理的过期时间,避免服务器已失效但本地仍尝试刷新。
  • 在TokenManager中增加对Refresh Token有效期的检查逻辑。

3.2 错误处理与降级策略

  • 网络异常或服务器错误时,需返回原始401错误,避免无限重试。
  • 可通过重试计数器限制最大重试次数。

3.3 测试验证要点

  • 单元测试:模拟401响应,验证拦截器是否触发刷新。
  • 集成测试:在弱网环境下测试并发请求的刷新行为。
  • 压力测试:模拟高并发场景,检查锁机制是否生效。

3.4 性能优化方向

  • 使用Dispatchers.IO调度Token刷新任务,避免阻塞主线程。
  • 对频繁请求的API,可考虑预加载Token(如应用启动时检查有效期)。

四、方案优势总结

  1. 无感知体验:用户无需手动重登,业务请求自动恢复。
  2. 低延迟:协程的轻量级特性减少线程切换开销。
  3. 高可靠性:通过并发控制避免重复刷新,确保数据一致性。
  4. 可扩展性:易于集成至现有网络层,支持多种认证协议(如OAuth2、JWT)。

五、进阶思考:与百度智能云的结合

若应用后端部署于百度智能云,可进一步优化方案:

  • 利用百度智能云的API网关服务,在网关层实现Token验证与刷新,减少客户端逻辑。
  • 结合百度智能云的密钥管理服务(KMS),安全存储Refresh Token,避免本地泄露风险。

通过拦截器与协程的深度融合,开发者能够构建出既稳定又高效的Token管理方案,为Android应用提供无缝的认证体验。