原理: 客户端会持有两个Token令牌:accessToken和refreshToken,accessToken用于接口访问控制,每次访问接口都会携带验证,并定期刷新;refreshToken用于登录超时控制,同时用于accessToken超时验证,当accessToken刷新时,携带refreshToken校验其合法性;同样refreshToken也有时效性,超时需要重新登录。双Token机制避免了频繁登录验证,提高用户体验。
难点: 高并发情况下的token同步刷新问题。
以retrofit为例,通过添加拦截器实现双Token验证功能。
/**
* Token拦截器
*/
class TokenInterceptor: Interceptor {
private val tagHeader = "TokenInterceptor-"
private val refreshTokenUrlPathHeader = "${RemoteClient.devUrl}/refreshToken"
private fun accessToken() =
SharedPreferenceUtil.findSharedPreference(SharedPreferenceUtil.KEY_ACCESS_TOKEN, "")
override fun intercept(chain: Chain): Response {
val tag = Constants.LOG_NET_HEADER.plus(tagHeader).plus("intercept:")
val curAccessToken = accessToken()
chain.proceed(newRequest(chain, curAccessToken))
.apply {
if (!isSuccessful) {
body()?.string()?.let {
Gson().fromJson(it, TokenTimeoutModel::class.java)
.let { tokenResult ->
if (tokenResult.code == CODE_201) {
//token超时
Log.e(tag, "accessToken timeout")
//newAccessToken双重校验锁
var newAccessToken = accessToken()
if (newAccessToken == curAccessToken) {
synchronized(this@TokenInterceptor) {
newAccessToken = accessToken()
if (newAccessToken != curAccessToken) {
//accessToken已经刷新,续接请求
Log.d(tag, "accessToken已经刷新,续接请求")
this.close()
return chain.proceed(
newRequest(
chain,
newAccessToken
)
)
} else {
//刷新accessToken
val userName: String =
SharedPreferenceUtil.findSharedPreference(
SharedPreferenceUtil.KEY_ACCOUNT_NAME,
""
)
val refreshToken: String =
SharedPreferenceUtil.findSharedPreference(
SharedPreferenceUtil.KEY_REFRESH_TOKEN,
""
)
userName.let {
Log.d(tag, "userName: $userName")
if (userName.isNotEmpty()) {
refreshToken.let { refreshToken ->
if (refreshToken.isNotEmpty()) {
Log.e(tag, "refreshToken start")
this.close()
chain.request()
.newBuilder()
.method("GET", null)
.url("$refreshTokenUrlPathHeader/${userName}/${refreshToken}")
.build()
.let { newRequest ->
chain.proceed(newRequest)
.let { refreshTokenResponse ->
if (refreshTokenResponse.code() == 200) {
Gson().fromJson(
refreshTokenResponse.body()
?.string(),
TokenModel::class.java
)
.let { tokenModel ->
if (tokenModel.code == CODE_201) {
//跳转登录页面,重新登录
Log.e(
tag,
"refreshToken过期,重新登录"
)
throw RefreshTokenTimeoutException()
} else {
Log.e(
tag,
"使用新的AccessToken访问接口: ${tokenModel.result.accessToken}"
)
//使用新的accessToken访问接口,本地缓存新的token
localCache(
tokenModel.result.accessToken
)
return chain.proceed(
newRequest(
chain,
tokenModel.result.accessToken
)
)
}
}
} else {
return refreshTokenResponse
}
}
}
} else {
//跳转登录页面,重新登录
Log.e(tag, "refreshToken没存储,重新登录")
throw RefreshTokenTimeoutException()
}
}
} else {
//跳转登录页面,重新登录
Log.e(tag, "userName没存储,重新登录")
//用户名没存储,重新登录
throw RefreshTokenTimeoutException()
}
}
}
}
} else {
//accessToken已经刷新,续接请求
Log.d(tag, "accessToken已经刷新,续接请求")
this.close()
return chain.proceed(newRequest(chain, newAccessToken))
}
} else {
//token没有超时
return this.newBuilder()
.body(ResponseBody.create(body()?.contentType(), it))
.build()
}
}
} ?: return this
} else {
return this
}
}
}
}