共计 2813 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点
在移动应用开发中,Token 管理是一个常见但容易出错的环节。尤其是在用户量大的情况下,Token 的管理不当会导致一系列问题:

- Token 过期处理 :当 Token 过期时,如果没有合理的刷新机制,用户会被迫重新登录,体验极差。
- 并发请求导致的重复刷新 :多个请求同时发现 Token 过期,可能会触发多次刷新请求,导致资源浪费甚至服务端异常。
- 安全性问题 :Token 的存储和传输如果不加密,容易被中间人攻击窃取。
这些问题不仅影响用户体验,还可能引发安全问题。因此,我们需要一个高效且安全的 Token 管理方案。
技术对比:Cookie、Session 与 Token
在移动端开发中,常见的身份验证方式有 Cookie、Session 和 Token。以下是它们的优劣对比:
- Cookie:
- 优点:浏览器自动管理,开发简单。
-
缺点:依赖浏览器环境,移动端兼容性差;容易被 CSRF 攻击。
-
Session:
- 优点:服务端管理状态,安全性较高。
-
缺点:服务端需要存储 Session 数据,扩展性差;移动端频繁切换网络时可能丢失 Session。
-
Token(如 JWT):
- 优点:无状态,服务端无需存储;适合移动端和分布式系统;支持跨域。
- 缺点:Token 一旦签发无法撤销,需设置较短的过期时间。
显然,Token 方案更适合移动场景,尤其是需要支持多端登录的应用。
核心实现
使用 OkHttp 拦截器实现自动 Token 注入
OkHttp 的拦截器可以在请求发出前和响应返回后插入自定义逻辑。我们可以通过拦截器自动为每个请求添加 Token:
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${tokenManager.getToken()}")
.build()
return chain.proceed(request)
}
}
基于 Retrofit 的 Authenticator 处理 401 状态码
当服务端返回 401(未授权)时,我们可以通过 Retrofit 的 Authenticator 自动刷新 Token 并重试请求:
class TokenAuthenticator(private val tokenManager: TokenManager) : Authenticator {override fun authenticate(route: Route?, response: Response): Request? {
// 避免重复刷新
if (responseCount(response) >= 3) {return null}
// 同步刷新 Token(注意:实际项目中应异步刷新)val newToken = tokenManager.refreshToken()
return response.request().newBuilder()
.header("Authorization", "Bearer $newToken")
.build()}
private fun responseCount(response: Response): Int {
var count = 1
var currentResponse = response.priorResponse()
while (currentResponse != null) {
count++
currentResponse = currentResponse.priorResponse()}
return count
}
}
线程安全的 Token 刷新机制
为了避免并发请求导致的多次刷新,我们需要一个线程安全的刷新机制。以下是基于 Kotlin 协程的实现:
class TokenManager {private val refreshLock = Mutex()
private var isRefreshing = false
suspend fun refreshToken(): String {
// 加锁,防止并发刷新
refreshLock.withLock {if (isRefreshing) {
// 等待其他协程完成刷新
while (isRefreshing) {delay(100)
}
return getToken()}
isRefreshing = true
try {val newToken = apiService.refreshToken()
saveToken(newToken)
return newToken
} finally {isRefreshing = false}
}
}
}
安全考量
Token 存储方案
Token 的存储需要兼顾安全性和便捷性。常见的方案有:
- EncryptedSharedPreferences:
- 优点:使用方便,适合大多数场景。
-
缺点:密钥存储在设备上,Root 设备可能被破解。
-
KeyStore:
- 优点:密钥由系统硬件保护,安全性最高。
- 缺点:实现复杂,兼容性可能有问题。
推荐在非金融类应用中使用 EncryptedSharedPreferences,而在高安全要求的场景下使用 KeyStore。
防止中间人攻击的证书锁定
为了防止中间人攻击,我们可以通过证书锁定(Certificate Pinning)确保客户端只信任特定的证书:
val certificatePinner = CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
避坑指南
避免在拦截器中同步刷新 Token
拦截器中同步刷新 Token 会导致请求阻塞,甚至引发死锁。正确的做法是在 Authenticator 中异步刷新。
处理多进程场景下的 Token 共享
如果应用有多个进程(如主进程和推送进程),需要使用跨进程的存储方案(如 ContentProvider)共享 Token。
性能优化
减少序列化 / 反序列化开销
Token 的解析(如 JWT)可能涉及多次序列化和反序列化。可以通过缓存解析结果减少开销。
合理的 Token 过期时间设置
- 访问 Token(Access Token):建议设置为几分钟到几小时。
- 刷新 Token(Refresh Token):建议设置为几天到几周。
结尾
通过上述方案,我们可以实现一个高效且安全的 Token 管理模块。但实际项目中,可能还需要支持多登录方式(如微信、手机号、邮箱)。如何设计一个支持多登录方式的 Token 工厂模式?这是一个值得思考的问题。
