找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1209

积分

0

好友

153

主题
发表于 2025-12-17 23:18:27 | 查看: 75| 回复: 0

在移动应用开发中,管理用户身份验证令牌(Token)的过期与刷新是一个常见的挑战。一个健壮的刷新机制能有效提升用户体验,避免因Token失效导致的频繁登录。本文将分享一种基于OkHttp拦截器与协程队列的实现方案。

核心设计思路

该方案的核心是构建一个自定义的OkHttp拦截器。其工作流程如下:

  1. 拦截响应:在拦截器中检查HTTP响应状态码。
  2. 捕获401:当首次遇到401 Unauthorized状态码时,触发Token刷新流程。
  3. 队列管理:在刷新Token期间,后续所有同样返回401的请求将被暂存到一个等待队列中,避免重复刷新。
  4. 重试请求:待Token刷新成功后,使用新令牌自动重放等待队列中的所有请求。
  5. 失败处理:若刷新失败,则清空队列并引导用户重新登录。

这种设计确保了在高并发请求场景下,Token刷新操作只会执行一次,后续请求会得到妥善处理。

代码实现详解

1. Token刷新拦截器

以下是拦截器TokenRefreshInterceptor的核心实现代码:

class TokenRefreshInterceptor(
    private val client: OkHttpClient,
    private val loginLauncher: () -> Unit
) : Interceptor {
    private val lock = Any()
    private var refreshJob: Job? = null
    // 待重试的请求队列(并发安全)
    private val pendingRequests = mutableListOf<Request>()
    private val noRefreshClient by lazy {
        client.newBuilder()
            .addInterceptor(HttpHeaderInterceptor()) // 仅业务头
            .addInterceptor(LoggingInterceptor(true))
            .eventListener(TimingEventListener())
            .build()
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val response = chain.proceed(original)
        if (response.code == 401 ) {
                synchronized(lock) {
                    if (refreshJob != null) {
                        // 已有刷新在进行,入队等待
                        pendingRequests.add(original)
                        return@synchronized
                    } else {
                        // 首次触发刷新
                        pendingRequests.add(original)
                        refreshJob = CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
                            try {
                                val freshToken = performRefresh()
                                if (freshToken.isNullOrBlank()) {
                                    // 刷新失败,跳转登录
                                    withContext(Dispatchers.Main) { loginLauncher() }
                                    return@launch
                                }
                                // 刷新完成:重放队列(续传 body)
                                val retryList = synchronized(lock) {
                                    pendingRequests.toList().also { pendingRequests.clear() }
                                }
                                retryList.forEach { req ->
                                    try {
                                        val retryReq = req.newBuilder()
                                            .header("Authorization", "Bearer $freshToken")
                                            .method(req.method, req.body)
                                            .build()
                                        noRefreshClient.newCall(retryReq).execute()
                                    } catch (e: Exception) {
                                        // 记录或上报失败
                                    }
                                }
                            } catch (e: Exception) {
                                // 异常兜底:清空队列并跳转登录
                                pendingRequests.clear()
                                withContext(Dispatchers.Main) { loginLauncher() }
                            } finally {
                                synchronized(lock) { refreshJob = null }
                            }
                        }
                    }
                }
            }
        return response
    }
    //刷新Token
    private suspend fun performRefresh(): String? = withContext(Dispatchers.IO)  {
        try {
            val userToken = UserManager.getUserToken()
            if (userToken.isBlank()) return@withContext null
            // ApiRetrofit是一个自定义的Retrofit工具类
            val api = ApiRetrofit.refreshTokenRetrofit().create(RefreshApi::class.java)
            val resp = api.refreshToken(userToken)
            return@withContext if (resp.isSuccess) {
                resp.respData?.token?.also { token ->
                    UserManager.setUserToken(token) // 保存新Token到本地
                }
            } else {
                null
            }
        } catch (e: Exception) {
            null
        }
    }
    interface RefreshApi {
        @GET("/refresh/token")
        suspend fun refreshToken(@Query("token") token: String): ResponseResult<RefreshResponse>
    }
    data class RefreshResponse(val expireTime: String, val token: String)
}

(上述代码逻辑示意图)
拦截器逻辑图

2. Retrofit工具类配置

要使拦截器生效,需要在OkHttpClient的构建过程中正确添加它。顺序至关重要:刷新拦截器必须添加在其他业务拦截器(如添加通用Header的拦截器)之前

open class RetrofitUtil {
    companion object {
        // 全局登录跳转回调
        @JvmStatic
        var loginLauncher: () -> Unit = {}
        @JvmStatic
        var appContext: Context? = null

        // 线程安全的强制登出
        @JvmStatic
        fun forceLogout() {
            // ... 清理本地Token并跳转登录页
        }
    }

    private val oKhttpBuilderNoCache: OkHttpClient.Builder
        get() {
            val okhttpClientBuilder = OkHttpClient.Builder()
                .connectTimeout(NetConstant.DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
                // 1) 刷新拦截器在最前
                .addInterceptor(
                    TokenRefreshInterceptor(
                        client = baseOkHttpClient.build(),
                        loginLauncher = { forceLogout() } // 刷新失败时触发登出
                    )
                )
                // 2) 业务头拦截器
                .addInterceptor(HttpHeaderInterceptor())
                .addInterceptor(LoggingInterceptor(true))
            // ... SSL等其他配置
            return okhttpClientBuilder
        }

    // 用于构建普通API请求的Retrofit
    fun baseRetrofitNoCache(): Retrofit.Builder {
        return Retrofit.Builder()
            .client(oKhttpBuilderNoCache.build())
            .baseUrl(baseUrl)
            .addConverterFactory(create())
    }

    // 基础 OkHttpClient(供刷新专用)
    private val baseOkHttpClient : OkHttpClient.Builder
        get() {
            // 构建一个不包含Token刷新拦截器的Client,防止循环调用
            val okhttpClientBuilder = OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .addInterceptor(LoggingInterceptor(true))
            // ... SSL配置
            return okhttpClientBuilder
        }

    // 刷新专用的 Retrofit(不带刷新拦截器,避免循环依赖)
    fun refreshTokenRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(baseOkHttpClient.build()) // 关键:使用不包含自身拦截器的Client
            .addConverterFactory(create())
            .build()
    }
}

(网络层配置示意图)
配置示意图

方案对比与总结

尽管上述拦截器方案设计精巧,能够自动、集中地处理Token刷新,但在实际团队协作中,复杂度可能成为考量的因素。最终,我们项目采用了另一种更显式、由服务器驱动的方案:

  1. 服务器预告:服务器在Token即将过期时,在响应中返回特定标识(非401状态)。
  2. 客户端响应:客户端拦截到这个标识后,在后台静默调用刷新接口更新本地Token。
  3. 过期处理:只有当Token完全过期且请求返回401时,才跳转到登录页面。

这种方案将刷新时机提前,避免了用户感知到的请求失败,但需要前后端协议配合。而拦截器方案更侧重于失败后的自动恢复,两者各有适用场景。在Android应用开发中,选择哪种方案取决于具体的业务需求、团队技术栈以及对用户体验的权衡。理解OkHttp拦截器机制本身,对于构建稳健的网络层至关重要。




上一篇:NVIDIA cuTile编程模型深度解析:Tile范式挑战Triton,重塑GPU算子开发门槛
下一篇:Web前端WebGPU与WASM协同工作原理解析与性能优化实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 07:37 , Processed in 0.387409 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表