Android并发场景下的Token失效问题:原理剖析与实战解决方案

1次阅读
没有评论

共计 2096 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

背景痛点:为什么 Token 在并发场景下容易失效?

在开发 Android 应用时,我们经常会遇到这样的场景:用户快速滑动列表触发分页加载,或者同时打开多个 Tab 发送并行请求。这时候如果 Token 突然过期,可能会导致:

Android 并发场景下的 Token 失效问题:原理剖析与实战解决方案

  • 所有并发请求都收到 401 未授权错误
  • 应用频繁弹出登录界面打断用户操作
  • 后台同时发起多个 Token 刷新请求造成服务端压力

传统单 Token 机制的核心问题在于:

  1. 竞态条件:当多个请求同时发现 Token 过期时,会各自尝试刷新 Token,导致服务端收到重复的刷新请求
  2. 时序混乱:后刷新的 Token 可能覆盖先刷新的 Token,使得部分请求仍然使用已失效的凭证
  3. 用户体验差:直接弹出登录框会打断用户当前操作流程

技术方案选型:三种主流解决方案对比

1. 单 Token 刷新机制

  • 优点:实现简单,适合低并发场景
  • 缺点
  • 无法处理并发刷新冲突
  • 需要客户端精确同步 Token 过期时间

2. 请求队列方案

  • 优点:能保证请求顺序执行
  • 缺点
  • 增加请求延迟
  • 需要维护复杂的队列状态

3. 双 Token 机制(推荐方案)

采用 AccessToken+RefreshToken 组合:

  • AccessToken:短期有效(如 2 小时),用于业务请求认证
  • RefreshToken:长期有效(如 7 天),专门用于获取新 AccessToken

工作流程

  1. AccessToken 过期时,用 RefreshToken 获取新 AccessToken
  2. 获取成功后更新本地存储
  3. 失败的请求自动用新 Token 重试

代码实现:基于 OkHttp Interceptor 的完整解决方案

以下是核心拦截器实现代码(Kotlin):

class TokenRefreshInterceptor : Interceptor {
    @Volatile private var isRefreshing = false
    private val lock = Any()

    override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()
        var response = chain.proceed(withAccessToken(request))

        // 检测到 Token 过期
        if (response.code == HTTP_UNAUTHORIZED) {synchronized(lock) {if (isRefreshing) {
                    // 等待其他线程完成刷新
                    while (isRefreshing) {lock.wait()
                    }
                    return chain.proceed(withAccessToken(request))
                }

                isRefreshing = true
                try {val newToken = refreshToken()
                    saveNewToken(newToken)
                    lock.notifyAll()
                    return chain.proceed(withAccessToken(request))
                } finally {isRefreshing = false}
            }
        }
        return response
    }

    private fun refreshToken(): String {
        // 实现 RefreshToken 的获取逻辑
        // 建议添加指数退避重试机制
    }
}

关键点说明:

  1. 使用 @Volatile 保证多线程可见性
  2. 同步块内双检锁避免重复刷新
  3. wait()/notifyAll()实现线程通信
  4. 失败请求自动重试无需用户干预

进阶优化:性能与安全考量

刷新策略优化

  • 预刷新:在 Token 即将过期前主动刷新(需配合过期时间预测)
  • 被动刷新:遇到 401 时再刷新(实现简单但体验稍差)

指数退避算法

当刷新失败时,建议按以下间隔重试:

  1. 首次失败:等待 1 秒
  2. 第二次失败:等待 2 秒
  3. 第三次失败:等待 4 秒

安全存储方案

val encryptedPrefs = EncryptedSharedPreferences.create(
    "token_store",
    MasterKey.Builder(context).build(),
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

避坑指南:常见问题与解决方案

  1. 不要直接在拦截器中弹登录窗
  2. 使用 EventBus 或 LiveData 发送全局事件
  3. 由 BaseActivity 统一处理跳转逻辑

  4. 服务端原子性保证

  5. 更新 AccessToken 时应使旧 Token 立即失效
  6. 可以考虑添加 jti(JWT ID)字段控制

  7. 跨进程共享方案

  8. 使用 ContentProvider 封装 Token 访问
  9. 或者通过文件锁实现进程间同步

思考与实践

尝试回答以下问题来检验学习效果:

  1. 如何设计 Benchmark 来比较不同方案的性能差异?
  2. 当用户长时间不操作导致 RefreshToken 也过期时,应该如何优雅处理?
  3. 在 Jetpack Compose 架构下,Token 管理应该如何与 UI 层解耦?

建议在实际项目中添加性能监控,记录:

  • Token 刷新成功率
  • 平均等待时间
  • 并发冲突发生率

这些数据可以帮助你持续优化认证流程。

正文完
 0
评论(没有评论)