/ 今日科技快讯 /
近日,在第十五届中国国际航空航天博览会上,中国国际航空股份有限公司(以下简称“国航”)与中国商用飞机有限责任公司(以下简称“中国商飞”)签署C929客机首家用户框架协议,意向成为C929宽体客机的全球首家用户。
/ 作者简介 /
本篇文章转自江天一色无纤尘的博客,文章主要分享了使用拦截器结合协程实现无感知Token预刷新,相信会对大家有所帮助!
https://juejin.cn/post/7408851018500833334
/ 前言 /
在应用中,我们通常使用 Token 作为用户认证的凭证。为了安全起见,Token 一般设置较短的有效期,并通过 refreshToken 进行续期。传统的做法是当服务端返回 Token 过期的响应(如 401)时,再进行刷新,但这种方式可能导致用户体验不佳(如突然的登录状态丢失、请求失败等)。网上关于 Android 开发中 Token 的无感刷新文章也比较少,且大多是请求失败再进行刷新。因此,我这里提供一种预刷新方案,在 Token 接近过期时提前进行刷新。
/ Token刷新相关参数 /
首先简要说明一下有关 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 可以实现挂起并等待结果返回。
通过一系列的挂起,在if (currentTime.isAfter(refreshTime))条件内实现了对 Token 的请求刷新,刷新完毕后 builder 会添加最新的 Token 继续执行当前被挂起的网络请求。
流程图
误区提示
注意不要使用相同的 Retrofit 实例构建 refreshApi 和正常请求的代理对象。正常请求的 Retrofit 对象中包含自定义的 TokenInterceptor 拦截器,如果 refreshApi 也使用了这个 Retrofit 对象,刷新 Token 的请求将被拦截器捕获,导致递归调用并陷入无限循环。因此,必须使用不带 TokenInterceptor 的 Retrofit 实例来构建刷新 Token 的代理对象。
/ 总结 /
实测结果显示,在 Token 过期时,多个请求并发执行刷新逻辑时,用户几乎不会察觉到任何延迟。通过这种无感知的 Token 预刷新方案,可以有效减少 Token 过期带来的请求失败问题,同时提升了用户的体验。如果你也在处理类似的需求,希望这个方案能给你带来帮助,也欢迎一起讨论实现方案和技术细节。
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注