背景
在应用中,我们通常使用 Token 作为用户认证的凭证。为了安全起见,Token 一般设置较短的有效期,并通过 refreshToken
进行续期。传统的做法是当服务端返回 Token 过期的响应(如 401)时,再进行刷新,但这种方式可能导致用户体验不佳(如突然的登录状态丢失、请求失败等)。网上关于 Android 开发中 Token 的无感刷新文章也比较少,且大多是请求失败再进行刷新。因此,我这里提供一种预刷新方案,在 Token 接近过期时提前进行刷新。
Token 刷新相关参数
首先简要说明一下有关 Token 刷新的几个参数。
-
Access Token(访问令牌):一种用于验证客户端请求的短期凭证。当用户登录时,服务器会生成一个访问令牌并将其发送给客户端。客户端在每次请求时将该令牌包含在请求头中,以证明用户的身份。由于访问令牌的有效期较短,因此需要定期刷新。
-
Refresh Token(刷新令牌):一种用于获取新访问令牌的长期凭证。与访问令牌不同,刷新令牌通常具有较长的有效期。当访问令牌过期时,客户端可以使用刷新令牌请求服务器颁发一个新的访问令牌,而无需让用户重新登录。
-
过期时间:指的是访问令牌和刷新令牌的有效期。访问令牌的过期时间较短,一般为几分钟到一小时不等,而刷新令牌的过期时间较长,通常为几天、几周甚至更久。合理设置过期时间能够在确保安全性的同时,提升用户体验,减少频繁登录的需求。为了进一步提升安全性,当刷新令牌也过期时,用户通常需要重新登录以获取新的凭证。
实现思路
我的目标是确保 Token 在接近过期时无感刷新,避免用户因 Token 过期而体验到任何中断。首先定义一个拦截器,继承自 Interceptor
,在这里实现Token的刷新逻辑,并把该拦截器添加到 Retrofit
的拦截器链中。在拦截器中会检查当前的 Token 是否快要过期,如果是,则提前刷新 Token。
具体思路如下:
-
提前刷新时间计算:
在每次请求之前,都会检查 Token 的有效期。如果发现 Token 即将在 5 分钟内过期,就会进行刷新。 -
双重锁检查:
为了避免多个请求同时触发 Token 刷新,导致并发问题,这里使用了双重锁检查的方式来确保只有一个线程会进行 Token 刷新。 -
刷新失败处理:
如果 Token 刷新失败,会引导用户重新登录,并确保用户可以继续正常使用应用。
代码实现
以下是 TokenInterceptor
的具体实现代码:
class TokenInterceptor : Interceptor {
@RequiresApi(Build.VERSION_CODES.O)
override fun intercept(chain: Interceptor.Chain): Response {
val tokenBean = CacheUtil.getToken()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
// 检查Token过期时间
if (!tokenBean.expireTime.isNullOrEmpty()) {
val expireTime = tokenBean.expireTime.substring(0, 19).let { LocalDateTime.parse(it, formatter) }
val currentTime = LocalDateTime.now()
// 提前5分钟刷新Token
val refreshTime = expireTime.minus(5, ChronoUnit.MINUTES)
if (expireTime != null && currentTime.isAfter(refreshTime)) {
synchronized(this) {
if (currentTime.isAfter(refreshTime)) { // 双重锁检查
runBlocking {
val newToken = refreshAuthToken(tokenBean.refreshToken ?: "")
newToken?.let { token ->
CacheUtil.setToken(token)
}
}
}
}
}
}
// 添加Token到请求头
val builder = chain.request().newBuilder().apply {
addHeader("token", CacheUtil.getToken().token ?: "")
}
return chain.proceed(builder.build())
}
private suspend fun refreshAuthToken(refreshToken: String): Token? {
return withContext(Dispatchers.IO) {
try {
val response = refreshApi.refreshToken(refreshToken)
if (response.code == 200 && response.data != null) {
response.data
} else { // refreshToken过期等失败情况
handleTokenRefreshFailure()
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
private fun handleTokenRefreshFailure() {
//处理失败情况,跳转到登录界面
}
}
协程关键点解析
- 该拦截器运行在子线程中,通过
runBlocking
阻塞当前线程并调用refreshAuthToken
方法,挂起当前协程,等待newToken
返回。 withContext
同样会挂起当前函数,使得refreshAuthToken
能够返回withContext(Dispatchers.IO)
中获取的结果。refreshApi.refreshToken(refreshToken)
是一个挂起函数,结合Retrofit
可以实现挂起并等待结果返回。
误区提示
注意不要使用相同的 Retrofit
实例构建 refreshApi
和正常请求的代理对象。 正常请求的 Retrofit
对象中包含自定义的 TokenInterceptor
拦截器,如果 refreshApi
也使用了这个 Retrofit
对象,刷新 Token 的请求将被拦截器捕获,导致递归调用并陷入无限循环。因此,必须使用不带 TokenInterceptor
的 Retrofit
实例来构建刷新 Token 的代理对象。
总结
实测结果显示,在 Token 过期时,多个请求并发执行刷新逻辑时,用户几乎不会察觉到任何延迟。通过这种无感知的 Token 预刷新方案,可以有效减少 Token 过期带来的请求失败问题,同时提升了用户的体验。如果你也在处理类似的需求,希望这个方案能给你带来帮助,也欢迎一起讨论实现方案和技术细节。
标签:令牌,拦截器,协程,请求,过期,Token,刷新 From: https://blog.csdn.net/debug_ww/article/details/141905094