14、Token管理与自动刷新:拦截器自动附加Token、Token过期自动刷新、并发请求的Token同步问题
Token管理这件事,说简单也简单,说复杂也复杂。
我刚入行那会儿,觉得Token不就是往请求头里塞个字符串嘛,有什么好讲的?直到有一次线上事故——用户同时发起多个请求,Token恰好过期,结果所有请求都去刷新Token,服务器直接返回429限流,用户集体掉线。嗯,从那以后,我再也不敢小看Token管理了。
14.1 拦截器自动附加Token
先解决最基础的问题:怎么让每个请求都带上Token?
我个人习惯用OkHttp的Interceptor来做这件事。你想想看,如果每个API调用都手动传Token,那代码得多难看?而且万一哪天Token的存储位置变了,你得改多少个地方?
class AuthInterceptor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// 跳过不需要Token的请求(比如登录、注册)
if (shouldSkipAuth(originalRequest)) {
return chain.proceed(originalRequest)
}
val token = tokenProvider.getAccessToken()
val authRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(authRequest)
}
private fun shouldSkipAuth(request: Request): Boolean {
val path = request.url.encodedPath
return path.contains("/login") || path.contains("/register")
}
}
14.2 Token过期自动刷新
Token总会过期的,这是设计使然。问题在于:过期之后怎么办?
最粗暴的做法是:每次请求前检查Token是否过期,过期就刷新。但这样有个问题——你想想看,如果Token还有5分钟才过期,但用户正好在刷一个很长的列表,刷到一半Token过期了,那体验多差?
更好的做法是:让服务器告诉你Token过期了,然后你再去刷新。说白了就是利用HTTP 401状态码。
class TokenRefreshInterceptor(
private val tokenProvider: TokenProvider,
private val tokenRefresher: TokenRefresher
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// 只有401才触发刷新
if (response.code == 401 && !isRefreshRequest(request)) {
response.close()
// 同步刷新Token
val newToken = tokenRefresher.refreshToken()
if (newToken != null) {
// 用新Token重试
val retryRequest = request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(retryRequest)
}
}
return response
}
private fun isRefreshRequest(request: Request): Boolean {
return request.url.encodedPath.contains("/refresh")
}
}
14.3 并发请求的Token同步问题
这才是真正的难点。
假设用户同时发起5个请求,每个请求都带着过期的Token。服务器返回5个401,然后5个请求同时去刷新Token。结果呢?
- 要么服务器限流,刷新失败
- 要么刷新成功,但生成了5个新Token,只有最后一个有效
- 要么并发导致数据竞争,Token状态混乱
怎么解决?核心思路就一句话:同一时间只允许一个刷新请求执行,其他请求排队等待。
class TokenManager(
private val tokenApi: TokenApi,
private val tokenStore: TokenStore
) {
private val mutex = Mutex()
private var refreshInProgress = false
private var newToken: String? = null
suspend fun getValidToken(): String {
val currentToken = tokenStore.getAccessToken()
if (!isTokenExpired(currentToken)) {
return currentToken
}
// 加锁,确保只有一个协程执行刷新
mutex.withLock {
if (refreshInProgress) {
// 已经有其他协程在刷新,等待结果
return newToken ?: throw TokenRefreshException()
}
refreshInProgress = true
try {
val response = tokenApi.refreshToken(
tokenStore.getRefreshToken()
)
newToken = response.accessToken
tokenStore.saveAccessToken(newToken!!)
return newToken!!
} finally {
refreshInProgress = false
}
}
}
}
- 使用Mutex(互斥锁)保证同一时间只有一个刷新操作
- 用refreshInProgress标志位避免重复刷新
- 其他协程等待第一个刷新完成后直接复用结果
14.4 完整的拦截器链
把上面这些整合到一起,就形成了一个完整的Token管理方案。我画了一张图,你可以看看整个流程:
这张图里最关键的就是那个Mutex加锁的环节。我再说得直白一点:不管有多少个并发请求,刷新Token的操作只会执行一次。其他请求要么等,要么复用刷新结果。
14.5 避坑指南
我在项目中踩过不少坑,这里列几个最常见的:
- 刷新Token本身也会过期——Refresh Token也有有效期,一般比Access Token长。如果Refresh Token也过期了,那就只能让用户重新登录了。
- 不要用全局变量存Token——App切到后台再切回来,Token可能已经被其他进程刷新了。我建议用SharedPreferences或者DataStore,配合加密存储。
- 注意协程作用域——Token刷新操作应该在ViewModelScope或者ApplicationScope里执行,不要在某个Activity的Scope里做,否则Activity销毁了刷新就中断了。
- 处理刷新失败的情况——如果刷新接口返回了非200状态码,比如网络超时或者服务器错误,不要无限重试。设置一个最大重试次数,超过就提示用户重新登录。
14.6 总结
Token管理说白了就三件事:
- 用拦截器自动附加Token,别让业务代码操心这事
- 用401状态码触发刷新,别自己瞎猜过期时间
- 用Mutex解决并发问题,别让多个请求同时去刷新
做到这三点,你的Token管理基本就稳了。剩下的就是一些细节打磨,比如Token存储加密、刷新失败的重试策略、以及用户登录态的生命周期管理。这些我会在后面的章节里继续聊。