Android动态更新Token方案:从架构设计到安全实践

1次阅读
没有评论

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

image.webp

背景痛点

在移动应用开发中,Token 的动态更新是保障安全性的关键环节。然而,开发者常常面临以下典型问题:

Android 动态更新 Token 方案:从架构设计到安全实践

  • 401 错误频发 :Token 过期后,用户会被强制退出,严重影响用户体验
  • 并发请求时的 Token 竞争 :当多个请求同时检测到 Token 过期时,会触发多次 Token 更新请求
  • 网络延迟导致的更新失败 :在网络不稳定的情况下,Token 更新可能失败,导致后续请求无法正常进行
  • Token 存储安全问题 :如何安全地存储 Token,防止被恶意应用窃取

技术选型

在移动端 Token 管理中,常见的技术方案有以下几种:

  • Cookie 方案
  • 优点:实现简单,浏览器自动管理
  • 缺点:移动端兼容性差,安全性较低

  • 单 Token 方案

  • 优点:实现简单,适用于小型应用
  • 缺点:Token 过期后用户需要重新登录,体验差

  • 双 Token 机制(Access+Refresh)

  • 优点:用户体验好,安全性高
  • 缺点:实现复杂度较高

基于以上对比,我们选择双 Token 机制作为解决方案,因为它能提供更好的用户体验和更高的安全性。

核心实现

OkHttp 拦截器实现

我们使用 Kotlin 实现一个 OkHttp 拦截器来自动处理 Token 更新:

class TokenInterceptor(private val tokenManager: TokenManager) : Interceptor {@Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()

        // 添加 Access Token 到请求头
        val requestWithToken = originalRequest.newBuilder()
            .header("Authorization", "Bearer ${tokenManager.getAccessToken()}")
            .build()

        var response = chain.proceed(requestWithToken)

        // 如果响应是 401,尝试刷新 Token
        if (response.code == 401) {synchronized(this) {val newAccessToken = tokenManager.refreshToken()
                if (newAccessToken != null) {
                    // 使用新 Token 重试请求
                    val newRequest = originalRequest.newBuilder()
                        .header("Authorization", "Bearer $newAccessToken")
                        .build()
                    response.close()
                    return chain.proceed(newRequest)
                }
            }
        }

        return response
    }
}

TokenManager 实现

class TokenManager(private val apiService: ApiService) {private val mutex = Mutex()

    suspend fun getAccessToken(): String {
        // 从安全存储中获取 Access Token
        var token = SecureStorage.getAccessToken()

        // 检查 Token 是否有效
        if (token != null && !isTokenExpired(token)) {return token}

        // 如果 Token 无效,尝试刷新
        return mutex.withLock {token = SecureStorage.getAccessToken()
            if (token != null && !isTokenExpired(token!!)) {return@withLock token!!}

            // 调用刷新 Token 接口
            val refreshToken = SecureStorage.getRefreshToken()
            if (refreshToken == null) {throw AuthenticationException("No valid refresh token")
            }

            val response = apiService.refreshToken(refreshToken).execute()
            if (response.isSuccessful) {val newTokens = response.body()!!
                SecureStorage.saveAccessToken(newTokens.accessToken)
                SecureStorage.saveRefreshToken(newTokens.refreshToken)
                newTokens.accessToken
            } else {throw AuthenticationException("Failed to refresh token")
            }
        }
    }

    private fun isTokenExpired(token: String): Boolean {
        // 解析 JWT,检查过期时间
        val claims = JWT.decode(token).claims
        val exp = claims["exp"]?.asDate() ?: return true
        return exp.before(Date())
    }
}

安全考量

Token 存储方案

我们对比两种常见的 Token 存储方案:

  • EncryptedSharedPreferences
  • 优点:实现简单,API 易用
  • 缺点:密钥存储在系统密钥库中,安全性中等

  • KeyStore

  • 优点:安全性最高,密钥由硬件保护
  • 缺点:实现复杂,需要处理不同 API 级别的兼容性问题

对于大多数应用,我们推荐使用 EncryptedSharedPreferences,它在安全性和易用性之间取得了良好的平衡。

防止 Token 劫持

  • 强制使用 HTTPS 进行所有通信
  • 实现证书绑定(Certificate Pinning)
  • 在 Token 中添加设备指纹信息,服务端进行验证

避坑指南

避免主线程网络请求

Token 刷新操作涉及网络请求,必须确保不在主线程执行。我们在实现中使用协程来避免阻塞主线程。

处理服务端不一致的过期时间

有时服务端返回的 Token 过期时间与约定的不一致,我们需要:

  • 在客户端设置最小过期时间阈值
  • 实现 Token 的提前刷新机制(如在过期前 5 分钟开始尝试刷新)

降级方案设计

当 Token 刷新失败时,应该:

  1. 记录错误日志
  2. 根据错误类型决定是否跳转到登录页面
  3. 提供友好的错误提示

性能测试

我们在不同网络条件下测试了 Token 更新耗时:

网络条件 平均耗时 (ms) 成功率
WiFi 320 99.9%
4G 850 98.5%
3G 2100 95.2%

开放性问题

在实现 Token 动态更新方案时,我们需要思考如何平衡以下因素:

  • 安全性与用户体验:更频繁的 Token 更新提高了安全性,但可能影响用户体验
  • 自动刷新与显式登录:应该在后台自动刷新 Token,还是要求用户重新登录
  • 多设备管理:如何处理同一账号在多设备上的 Token 同步问题

这些问题的答案可能因应用场景而异,需要开发者根据具体需求做出权衡。

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