Retrofit & RxJava2实现401自动刷新Token并重试请求方案咨询
这个场景太常见了,我在做App的时候几乎每个项目都要处理这个逻辑——Retrofit请求遇到401自动刷token并重试,核心就是利用OkHttp的拦截器来做全局拦截,还要注意并发请求的情况,避免重复调用刷新接口。下面给你详细讲怎么实现:
核心思路
- 用OkHttp的
Interceptor拦截所有出站请求,给请求带上当前有效的authToken - 当收到401响应时,判断是不是token过期导致的(排除刷新接口本身的401)
- 触发刷新token的请求,更新本地存储的
authToken和refreshToken - 用新的
authToken重新发起原请求 - 处理并发场景:多个请求同时遇到401时,只发起一次刷新请求,其他请求等待刷新完成后用新token重试
具体代码实现
首先定义你的刷新token接口和数据类:
// 刷新Token的API接口 interface AuthService { @POST("/auth/actions/refresh") suspend fun refreshToken(@Body request: RefreshTokenRequest): Response<TokenResponse> } // 请求体 data class RefreshTokenRequest(val refreshToken: String) // 响应体 data class TokenResponse(val authToken: String, val refreshToken: String)
然后写一个自定义的Token管理类,负责存储和读取token:
class TokenManager(private val sharedPrefs: SharedPreferences) { // 获取当前的authToken fun getAuthToken(): String = sharedPrefs.getString("auth_token", "") ?: "" // 获取refreshToken fun getRefreshToken(): String = sharedPrefs.getString("refresh_token", "") ?: "" // 保存新的authToken fun saveAuthToken(token: String) = sharedPrefs.edit().putString("auth_token", token).apply() // 保存新的refreshToken fun saveRefreshToken(token: String) = sharedPrefs.edit().putString("refresh_token", token).apply() // 清除所有token(刷新失败时用) fun clearTokens() = sharedPrefs.edit().remove("auth_token").remove("refresh_token").apply() }
接下来是核心的拦截器实现:
class AuthInterceptor( private val authService: AuthService, private val tokenManager: TokenManager ) : Interceptor { // 用锁和标志位控制并发刷新 private val refreshLock = Object() private var isRefreshing = false override fun intercept(chain: Interceptor.Chain): Response { var originalRequest = chain.request() val currentAuthToken = tokenManager.getAuthToken() // 给原请求添加Authorization头 originalRequest = originalRequest.newBuilder() .header("Authorization", "Bearer $currentAuthToken") .build() val originalResponse = chain.proceed(originalRequest) // 检查是否是401,并且不是刷新token的请求本身(避免死循环) if (originalResponse.code == 401 && !originalRequest.url.pathSegments.contains("refresh")) { synchronized(refreshLock) { // 双重检查:防止等待锁的过程中已经有其他线程刷新了token val newAuthToken = tokenManager.getAuthToken() if (newAuthToken != currentAuthToken) { // 已经有新token了,直接用新token重试原请求 val retryRequest = originalRequest.newBuilder() .header("Authorization", "Bearer $newAuthToken") .build() return chain.proceed(retryRequest) } // 没有新token,开始刷新 if (!isRefreshing) { isRefreshing = true // 因为intercept是同步方法,用runBlocking调用挂起的刷新接口 runBlocking { val refreshResponse = authService.refreshToken( RefreshTokenRequest(tokenManager.getRefreshToken()) ) if (refreshResponse.isSuccessful) { refreshResponse.body()?.let { tokenResp -> // 更新本地token tokenManager.saveAuthToken(tokenResp.authToken) tokenManager.saveRefreshToken(tokenResp.refreshToken) } } else { // 刷新失败:清除token,跳转到登录页(这里根据你的业务处理) tokenManager.clearTokens() // 可以抛异常或者返回原响应,让上层处理 return@runBlocking originalResponse } } isRefreshing = false } } // 刷新完成后,用新token重试原请求 val finalAuthToken = tokenManager.getAuthToken() val finalRetryRequest = originalRequest.newBuilder() .header("Authorization", "Bearer $finalAuthToken") .build() return chain.proceed(finalRetryRequest) } // 不是401,直接返回原响应 return originalResponse } }
最后把拦截器添加到OkHttp客户端,再构建Retrofit:
// 初始化TokenManager val tokenManager = TokenManager(context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE)) // 初始化AuthService(用来调用刷新接口) val authService = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(AuthService::class.java) // 构建带拦截器的OkHttp客户端 val okHttpClient = OkHttpClient.Builder() .addInterceptor(AuthInterceptor(authService, tokenManager)) .build() // 构建你的业务API服务 val yourApiService = Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 如果用协程的话 .build() .create(YourBusinessApi::class.java)
关键注意点
- 避免死循环:一定要排除刷新token的请求本身,不然如果刷新接口也返回401,会无限触发刷新逻辑
- 并发处理:用
synchronized和isRefreshing标志位,确保同时多个401请求只会发起一次刷新,避免重复调用刷新接口 - 双重检查token:在进入同步块后再次检查token,防止等待锁的过程中其他线程已经完成了刷新
- 失败处理:刷新token失败(比如refreshToken也过期了),要及时清除本地token,跳转到登录页面,避免无限重试
- 协程适配:因为拦截器的
intercept是同步方法,所以用runBlocking来调用挂起的刷新接口;如果用RxJava,处理逻辑类似,换成Observable的操作符即可
内容的提问来源于stack exchange,提问作者M Rajoy
相关产品推荐
相关产品推荐

