首页 > 其他分享 >Android 使用拦截器结合协程实现无感知的 Token 预刷新方案

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

时间:2024-09-04 21:51:28浏览次数:10  
标签:令牌 拦截器 协程 请求 过期 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 可以实现挂起并等待结果返回。

误区提示

注意不要使用相同的 Retrofit 实例构建 refreshApi 和正常请求的代理对象。 正常请求的 Retrofit 对象中包含自定义的 TokenInterceptor 拦截器,如果 refreshApi 也使用了这个 Retrofit 对象,刷新 Token 的请求将被拦截器捕获,导致递归调用并陷入无限循环。因此,必须使用不带 TokenInterceptorRetrofit 实例来构建刷新 Token 的代理对象。

总结

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

标签:令牌,拦截器,协程,请求,过期,Token,刷新
From: https://blog.csdn.net/debug_ww/article/details/141905094

相关文章

  • Codeforces LATOKEN Round 1 (Div. 1 + Div. 2)
    A.ColourtheFlag题意:给定一个棋盘,一些格子已经染上黑白色,判断能否将剩下的格子染色,使得相邻格子不同色。输出构造。思路:考虑一个棋盘的合法染色方案只有两种,分别比较一下即可。提交记录B.HistogramUgliness题意:一个柱状图,权值定义为操作次数加上竖直方向的周长。一次......
  • 【gtokentool】元宇宙nft区块链是什么
    元宇宙元宇宙的定义元宇宙(Metaverse)这个词起源于NealStephenson在1992年出版的小说《雪崩》,Metaverse由Meta(意即“超越”、“元”)和verse(意即“宇宙universe”)两个词构成。元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态,它是一个和现实世界有关联的虚......
  • Python教程(十七):协程、 asyncio与 aiohttp【异步IO】
    文章目录专栏列表1.异步IO的基本概念1.1同步与异步1.2协程1.3asyncio1.4aiohttp2.携程2.1定义协程2.2运行协程3.asyncio3.1事件循环解释3.2获取文件示例3.2并发获取文件示例4.aiohttp:异步HTTP客户端/服务器4.1安装aiohttp4.2异步HTTP请求4.3异......
  • keycloak~Refresh_token阶段不走RequiredAction
    Refresh_token是在access_token过期之后,用来换新的access_token的,有了Refresh_token之后,用户可以在很长一段时间不需要重新登录,这对于用户体验是有好处的;RequiredAction是一种登录阶段的必选行为,当一个用户被某个RequiredAction标记之后,用户必须完成RequiredAction,才算完成本次登......
  • Android协程的使用场景
    importkotlinx.coroutines.Dispatchersimportkotlinx.coroutines.delayimportkotlinx.coroutines.withContextsuspendfunallPlants():List<Plant>=withContext(Dispatchers.Default){delay(1500)valresult=sunflowerService.getA......
  • 【Java】Spring-AOP与拦截器实战 (上手图解)
    Java系列文章目录补充内容Windows通过SSH连接Linux第一章Linux基本命令的学习与Linux历史文章目录Java系列文章目录一、前言二、学习内容:三、问题描述四、解决方案:4.1认识依赖4.2使用AOP与拦截器4.2.1使用AOP4.2.1.1设置DemoAop类4.2.2.2设置切面4.2.2.3设......
  • php获取微信access_token
    参考代码:新建一个php文件,将下面代码拷贝进去,替换自己的appid和appSecret<?php//检查是否是GET请求if($_SERVER['REQUEST_METHOD']=='GET'){echogetAccessToken();}functiongetAccessToken(){$appId='替换';//微信小程序的AppID$appSecret=......
  • 使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析
    前言这篇文章将用三个版本的归并排序,为大家分析使用协程排序的时间开销(被排序的切片长度由128到1000w)本期demo地址:https://github.com/BaiZe1998/go-learning往期视频讲解......
  • Unity中的协程
            协程:让程序并行执行的功能。        什么是协程? 协程不是多线程,而是类似函数调用。    一.协程的基本用法usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;publicclassStudyCo:MonoBehaviour{......
  • 多线程篇(并发编程 - 进程&线程&协程&纤程&管程)(持续更新迭代)
    目录一、进程(Progress)1.进程2.僵尸进程2.1什么是僵尸进程2.2僵尸进程的危害2.3如何避免僵尸进程的产生3.参考链接二、线程(Thread)1.线程是什么?2.多线程2.1.概述2.2.多线程的好处2.3.多线程的代价3.线程模型(三种)3.1.一对一模型3.2.多对一模型3.3......