在移动应用开发中,管理用户身份验证令牌(Token)的过期与刷新是一个常见的挑战。一个健壮的刷新机制能有效提升用户体验,避免因Token失效导致的频繁登录。本文将分享一种基于OkHttp拦截器与协程队列的实现方案。
核心设计思路
该方案的核心是构建一个自定义的OkHttp拦截器。其工作流程如下:
- 拦截响应:在拦截器中检查HTTP响应状态码。
- 捕获401:当首次遇到
401 Unauthorized状态码时,触发Token刷新流程。
- 队列管理:在刷新Token期间,后续所有同样返回401的请求将被暂存到一个等待队列中,避免重复刷新。
- 重试请求:待Token刷新成功后,使用新令牌自动重放等待队列中的所有请求。
- 失败处理:若刷新失败,则清空队列并引导用户重新登录。
这种设计确保了在高并发请求场景下,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刷新,但在实际团队协作中,复杂度可能成为考量的因素。最终,我们项目采用了另一种更显式、由服务器驱动的方案:
- 服务器预告:服务器在Token即将过期时,在响应中返回特定标识(非401状态)。
- 客户端响应:客户端拦截到这个标识后,在后台静默调用刷新接口更新本地Token。
- 过期处理:只有当Token完全过期且请求返回401时,才跳转到登录页面。
这种方案将刷新时机提前,避免了用户感知到的请求失败,但需要前后端协议配合。而拦截器方案更侧重于失败后的自动恢复,两者各有适用场景。在Android应用开发中,选择哪种方案取决于具体的业务需求、团队技术栈以及对用户体验的权衡。理解OkHttp拦截器机制本身,对于构建稳健的网络层至关重要。
|