Android 使用拦截器结合协程实现无感知的 Token 预刷新方案

背景

在应用中,我们通常使用 Token 作为用户认证的凭证。为了安全起见,Token 一般设置较短的有效期,并通过 refreshToken 进行续期。传统的做法是当服务端返回 Token 过期的响应(如 401)时,再进行刷新,但这种方式可能导致用户体验不佳(如突然的登录状态丢失、请求失败等)。网上关于 Android 开发中 Token 的无感刷新文章也比较少,且大多是请求失败再进行刷新。因此,我这里提供一种预刷新方案,在 Token 接近过期时提前进行刷新。

Token 刷新相关参数

首先简要说明一下有关 Token 刷新的几个参数。

  • Access Token(访问令牌):一种用于验证客户端请求的短期凭证。当用户登录时,服务器会生成一个访问令牌并将其发送给客户端。客户端在每次请求时将该令牌包含在请求头中,以证明用户的身份。由于访问令牌的有效期较短,因此需要定期刷新。

  • Refresh Token(刷新令牌):一种用于获取新访问令牌的长期凭证。与访问令牌不同,刷新令牌通常具有较长的有效期。当访问令牌过期时,客户端可以使用刷新令牌请求服务器颁发一个新的访问令牌,而无需让用户重新登录。

  • 过期时间:指的是访问令牌和刷新令牌的有效期。访问令牌的过期时间较短,一般为几分钟到一小时不等,而刷新令牌的过期时间较长,通常为几天、几周甚至更久。合理设置过期时间能够在确保安全性的同时,提升用户体验,减少频繁登录的需求。为了进一步提升安全性,当刷新令牌也过期时,用户通常需要重新登录以获取新的凭证。

实现思路

我的目标是确保 Token 在接近过期时无感刷新,避免用户因 Token 过期而体验到任何中断。首先定义一个拦截器,继承自 Interceptor,在这里实现Token的刷新逻辑,并把该拦截器添加到 Retrofit 的拦截器链中。在拦截器中会检查当前的 Token 是否快要过期,如果是,则提前刷新 Token。

具体思路如下:

  1. 提前刷新时间计算
    在每次请求之前,都会检查 Token 的有效期。如果发现 Token 即将在 5 分钟内过期,就会进行刷新。

  2. 双重锁检查
    为了避免多个请求同时触发 Token 刷新,导致并发问题,这里使用了双重锁检查的方式来确保只有一个线程会进行 Token 刷新。

  3. 刷新失败处理
    如果 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 的请求将被拦截器捕获,导致递归调用并陷入无限循环。因此,必须使用不带 TokenInterceptorRetrofit 实例来构建刷新 Token 的代理对象。

总结

实测结果显示,在 Token 过期时,多个请求并发执行刷新逻辑时,用户几乎不会察觉到任何延迟。通过这种无感知的 Token 预刷新方案,可以有效减少 Token 过期带来的请求失败问题,同时提升了用户的体验。如果你也在处理类似的需求,希望这个方案能给你带来帮助,也欢迎一起讨论实现方案和技术细节。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值