在应用端和后端交互时,有些特定的接口需要验证token才能返回正确的结果,例如修改用户信息、获取用户信息等。token通常会设定一个有效期,在有效期内使用才能正常获取结果。当token过期时,用户体验可能受到影响,可以在token过期后自动刷新token优化用户体验。
本文简单介绍一下如何使用Interceptor
和协程实现自动刷新token。
实现自动刷新token
应用端通常会在请求一个需要校验token的接口失败后才感知到token过期。为了避免用户体验受到影响,可以使用OkHttp
的Interceptor
来拦截并修改发送请求和接收响应的过程。
实现方案如下:
- 自定义
Interceptor
,为需要验证token的接口添加header。 - 获取响应,判断token是否过期。
- 如果token过期,发起请求刷新token。如果多个需要验证token的接口并发执行,应该仅执行一次刷新token的操作。
- 刷新token后,携带新token重新发送原请求获取响应。
Interceptor
示例代码如下:
scss
class ApiInterceptor : Interceptor {
// 需要验证token的接口的路径集合
private val checkTokenInterface = arrayListOf(
"/example/user/info,
"/example/user/info/edit"
)
// 是否正在刷新token
// 使用Volatile,确保多线程获取的值一致
@Volatile
private var refreshingToken = false
private val authHeaderKey = "token-key"
override fun intercept(chain: Interceptor.Chain): Response {
// 根据接口路径判断是否需要验证token
val checkToken = checkTokenInterface.contains(chain.request().url.encodedPath)
// 需要验证token则添加请求头,不需要则保持原样
var request = if (checkToken) {
chain.request().newBuilder()
.addHeader(authHeaderKey, "tokenValue")
.build()
} else {
chain.request()
}
// 获取响应
var response = chain.proceed(request)
if (checkToken) {
// 需要验证token,处理响应,判断token是否过期
response.body?.let { responseBody ->
try {
if (判断token过期的条件) {
runBlocking {
if (!refreshingToken) {
// 标记为正在刷新token
refreshingToken = true
async {
refreshToken(chain).takeIf { it.isNotEmpty() && it.isNotBlank() }?.let { newToken ->
// 保存newToken
}
// 标记为刷新token已结束
refreshingToken = false
}.start()
}
// async{}.await()会等待方法块内的代码执行完毕。
// 等到refreshingToken为false时才会执行后续代码。
if (async { stopRefreshingToken() }.await()) {
// 携带newToken执行原请求获取响应
response = chain.proceed(chain.request().newBuilder()
.addHeader(authHeaderKey, "newToken")
.build())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// 返回最终的响应结果。
// 刷新token也可能失败,此时返回原响应,可以让用户重新登录。
return response
}
private fun refreshToken(chain: Interceptor.Chain): String {
// 创建刷新token的请求
val refreshTokenRequest = Request.Builder()
.url("refresh token url")
.addHeader(authHeaderKey, "old token")
.get()
.build()
return try {
// 获取响应并解析获得新token
val refreshTokenResponse = chain.proceed(refreshTokenRequest)
refreshTokenResponse.token
} catch (e: IOException) {
// 失败时返回空字符串
""
}
}
private suspend fun stopRefreshingToken(): Boolean {
return if (refreshingToken) {
// 仍在刷新token中,延迟1秒后再次执行此方法
delay(1000)
stopRefreshingToken()
} else {
true
}
}
}
PS: 部分代码涉及到公司的接口所以省略了,实际使用时需要针对需求进行适当的调整。