1、双token方式刷新token
对于token刷新,现在大部分公司都是用的accessToken和refreshToken这种双token方案,就是登录接口返回accessToken和refreshToken,业务请求header携带accessToken(有安全需求的,一般会对token进行签名),如果业务请求返回401表示accessToken过期,再header接待refreshToken调用刷新token的接口,如果刷新成功则用新的accessToken继续业务请求,如果刷新token失败则提示用户重新登录,时序图如下
2、OKHttp拦截器处理Token过期
双token要处理的问题大概有如下几点
- 1、当接口返回401时,调用刷新token接口
- 2、刷新成功后,重试业务请求
- 3、当有多个并发请求返回401时,避免多次刷新token
针对第一点可以使用OKhttp的Interceptor对Http请求的Response进行拦截,刷新成功后可以用新的accessToken重新构建新Request,再重试请求。对于并发请求返回401时只执第一个401,其他的挂起等第一个401请求刷新后再重试请求,可以通过加同步解决。
3、主要代码实现
自定义RefreshTokenInterceptor继承Interceptor,处理以上三个问题
import android.text.TextUtils
import android.util.Log
import com.cyq.http.bean.TokenBean
import com.google.gson.Gson
import okhttp3.*
/**
* @author : ChenYangQi
* date : 2021/1/10 23:50
* desc : 判断token过期并自动刷新,刷新成功后重试请求
*/
class RefreshTokenInterceptor : Interceptor {
companion object {
private const val TAG = "Token"
}
private val lock = Any()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
//判断Token是否过期
if (response.code() == 401) {
//通过同步锁和双重校验限制多个请求同时返回401时,只有第一个请求能执行刷新token的操作
if ((!TextUtils.isEmpty(TokenSingleton.instance.accessToken)) &&
TokenSingleton.instance.accessToken == request.header("accessToken")
) {
synchronized(lock) {
if (!TextUtils.isEmpty(TokenSingleton.instance.accessToken) && TokenSingleton
.instance.accessToken == request.header("accessToken")
) {
refreshToken(object : RefreshTokenCallBack {
override fun onFail(response: Response): Response {
Log.d(TAG, "token刷新失败-------")
//直接返回刷新token过期response.code=401的结果,应用层做重新登录的逻辑
return response
}
override fun onSuccess() {
Log.d(TAG, "token刷新成功----重试业务请求---")
}
})
}
}
}
//使用新的AccessToken继续业务请求
if (!TextUtils.isEmpty(TokenSingleton.instance.accessToken) &&
TokenSingleton.instance.accessToken != request.header("accessToken")
) {
Log.e(TAG, "获得新Token后重试")
val newRequest = request.newBuilder()
.header("accessToken", TokenSingleton.instance.accessToken)
.build()
//继续业务请求
return chain.proceed(newRequest)
} else {
throw RefreshTokenFailException("refresh token fail")
}
}
return response
}
/**
* 同步请求刷新Token
*/
private fun refreshToken(callback: RefreshTokenCallBack) {
Log.d(TAG, "进入刷新Token。。。。。")
val refreshToken = TokenSingleton.instance.refreshToken
if (TextUtils.isEmpty(refreshToken)) {
Log.d(TAG, "刷新Token失败!")
throw RefreshTokenFailException("refreshToken is empty,please first login")
}
val formBody = FormBody.Builder().build()
val request = Request.Builder()
.url("http://192.168.3.8:8083/accessToken/refresh")
.addHeader("refreshToken", refreshToken)
.post(formBody)
.build()
val call = OkHttpClient.Builder().build().newCall(request)
val response = call.execute()
val result = response.body()?.string()
val tokenBean: TokenBean = Gson().fromJson(result, TokenBean::class.java)
when {
response.code() == 200 -> {
Log.d(TAG, "Token刷新成功")
//刷新token成功,更新token
TokenSingleton.instance.accessToken = tokenBean.data.accessToken
TokenSingleton.instance.refreshToken = tokenBean.data.refreshToken
callback.onSuccess()
}
response.code() == 401 -> {
Log.e(TAG, "Token过期需要自动登录!")
callback.onFail(response)
}
else -> {
throw RefreshTokenFailException("refresh token fail")
}
}
}
interface RefreshTokenCallBack {
fun onFail(response: Response): Response
fun onSuccess()
}
}
测试代码,每隔10秒同时发送3个请求,测试用的服务端accessToken设置过期时间60秒,refresh设置时间90秒
Thread {
while (tokenEffective) {
//每10秒执行一次
Thread.sleep(10_000)
//获取用户信息接口
getUserInfo()
//测试求和接口
sum()
//测试乘法接口
multiply()
}
}.start()
服务端demo代码在项目跟目录下server_project
服务端demo使用步骤
1:安装redis,并启动redis(很简单,自行google)
2:idea打开项目(spring boot项目)
3:先运行AuthorizationService
4:再运行TokenDemoApplication启动服务
5:更换android项目中的接口地址为你PC的本地IP
android端demo代码地址:https://github.com/DaLeiGe/AndroidSamples/tree/master/HttpRequest