说明
在app使用过程中,登录中因为token过期的原因,可以需要重新登录,这样使用体验上会不好,所以会有产品要求token过期后自动刷新token,获取到新的token在继续请求接口,用户在使用上无感知,大大提升了用户体验。
技术实现
token一般我们会放到网络请求的header中
这样我们可以使用okhttp中拦截器,对网络请求进行拦截,获取到token过期的code码后,去请求刷新token,替换header请求中对于的token值,继续进行接口请求。
其中有一个问题需要注意
就是token过期时可以存在并发问题,这样我们就需要用同步锁进行处理,只做一次刷新token接口请求,并发过来的token及时替换成新的token,这就也避免了刷新token接口重复请求,节约性能。
直接撸代码
package com.tospur.module_base_component.commom.retrofit.interceptor;
import com.tospur.module_base_component.BaseApplication;
import com.tospur.module_base_component.commom.base.NormalHttpFilter;
import com.tospur.module_base_component.commom.retrofit.ApiStores;
import com.tospur.module_base_component.model.result.EventBusMsg;
import com.tospur.module_base_component.model.result.TokenResult;
import com.tospur.module_base_component.utils.EventBusUtils;
import com.tospur.module_base_component.utils.LogUtil;
import com.tospur.module_base_component.utils.SharedPreferenceUtil;
import com.tospur.module_base_component.utils.StringUtls;
import org.json.JSONObject;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.concurrent.TimeUnit;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import static com.alibaba.fastjson.util.IOUtils.UTF8;
/**
* @Description: 描述
* @Author: fugang
* @CreateDate: 2021/11/26 15:24
*/
public class TokenInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
TokenResult xToken = getTokenInfo();
Request request = chain.request();
Request.Builder requestBuilder = chain.request().newBuilder();
/*LogUtil.e("fff", "xToken===" + xToken);
if (xToken != null) {
requestBuilder.addHeader("authorization", xToken.getToken_type() + " " + xToken.getAccess_token()); //添加默认的Token请求头
LogUtil.e("fff", "authorization000000 " + xToken.getToken_type() + " " + xToken.getAccess_token());
}*/
Response proceed = chain.proceed(requestBuilder.build());
ResponseBody responseBody = proceed.body();
long contentLength = responseBody.contentLength();
if (!bodyEncoded(proceed.headers())) {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
try {
charset = contentType.charset(UTF8);
} catch (UnsupportedCharsetException e) {
return proceed;
}
}
if (!isPlaintext(buffer)) {
return proceed;
}
if (contentLength != 0) {
String result = buffer.clone().readString(charset);
//如果token过期 再去重新请求token 然后设置token的请求头 重新发起请求 用户无感
synchronized (this) {//同步代码块,当在刷新token的时候暂停其他的request,锁为当前类的单例对象
//比较请求的token与本地存储的token 如果不一致还是直接重试
TokenResult sToken = getTokenInfo();
String request_token = request.header("authorization");
String access_token =null;
if (sToken != null) {
access_token = sToken.getToken_type() + " " + sToken.getAccess_token();
}
LogUtil.d("TokenInterceptor", " request_token=="+request_token);
LogUtil.d("TokenInterceptor", " access_token=="+access_token);
if (access_token != null && !access_token.equals(request_token)) {
LogUtil.d("TokenInterceptor", " 用新token");
Request newRequest = request.newBuilder().header("authorization", access_token).build();//等待的request重新拼装请求头
return chain.proceed(newRequest);//重试request
}
if (isTokenExpired(result)) {
LogUtil.d("TokenInterceptor", " 11111token过期了");
String newToken = getNewToken(xToken.getRefresh_token());
LogUtil.d("TokenInterceptor", "newToken11111== " + xToken.getRefresh_token());
if (StringUtls.isNotEmpty(newToken)) {
TokenResult mToken = getTokenInfo();
LogUtil.d("TokenInterceptor", "mToken== " + newToken);
if (mToken != null) {
//使用新的Token,创建新的请求
Request newRequest;
newRequest = chain.request().newBuilder()
.header("authorization", mToken.getToken_type() + " " + mToken.getAccess_token())
.build();
return chain.proceed(newRequest);
}
}
}
}
}
}
return proceed;
}
/**
* 根据Response,判断Token是否失效
* 401表示token过期
*
* @param responseBody
* @return
*/
private boolean isTokenExpired(String responseBody) {
try {
JSONObject json = new JSONObject(responseBody);
int code = json.optInt("code");
if (code == 50405) {
return true;
}
} catch (Exception e) {
}
return false;
}
static boolean isPlaintext(Buffer buffer) throws EOFException {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyEncoded(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
}
/**
* 同步请求方式,获取最新的Token
*
* @return
*/
private synchronized String getNewToken(String refreshToken) throws IOException {
String newToken = "";
HttpLoggingInterceptor logInterceptor =new HttpLoggingInterceptor();
logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(logInterceptor)
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(TEST_API_SERVER_URL_DT)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build();
Call<ResponseBody> call = (Call<ResponseBody>) retrofit.create(ApiStores.class).refreshToken(refreshTokenMapParams(refreshToken));
retrofit2.Response<ResponseBody> tokenJson = call.execute();
String tokenResponse = new String(tokenJson.body().bytes());
try {
JSONObject json = new JSONObject(tokenResponse);
int code = json.optInt("code");
if (code == 200) {
newToken = json.optString("data");
cleanTokenInfo();
SharedPreferenceUtil.setValue(BaseApplication.instance(), "X-Token", newToken);
LogUtil.d("TokenInterceptor", "getNewToken== " + newToken);
}else{
LogUtil.d("TokenInterceptor", "token刷新接口请求失败="+json);
cleanTokenInfo();
EventBusUtils.getInstance().post(new EventBusMsg(NormalHttpFilter.RESULT_AUTHORIZATION));
}
LogUtil.d("TokenInterceptor", "getNewToken11111== " + tokenResponse);
} catch (Exception e) {
e.printStackTrace();
LogUtil.d("TokenInterceptor", "token刷新接口请求失败 e="+e.toString());
cleanTokenInfo();
EventBusUtils.getInstance().post(new EventBusMsg(NormalHttpFilter.RESULT_AUTHORIZATION));
}
return newToken;
}
}
上述是拦截器的具体实现,其中保存token是使用的单例模式,先存储到sharepreference,每次打开app只需要读取一次缓存中的值,保存为全局变量,避免多次读取缓存
@Volatile
var tokenResult: TokenResult? = null
fun saveTokenInfo(tokenInfo: TokenResult?) {
Log.e("BBB", "saveTokenInfo ")
BaseApplication.instance?.let {
tokenInfo?.let { info ->
SharedPreferenceUtil.setValue(it, "X-Token", GsonUtils().toJson(info))
}
}
}
@Synchronized
fun getTokenInfo(): TokenResult? {
Log.e("BBB", "getTokenInfo")
BaseApplication.instance?.let {
if (tokenResult == null) {
SharedPreferenceUtil.getValue(it, "X-Token", "").toString().let {
tokenResult = GsonUtils().fromJson(it, TokenResult::class.java)
}
}
}
return tokenResult
}
fun cleanTokenInfo() {
Log.e("BBB", "cleanTokenInfo")
tokenResult = null
BaseApplication.instance?.let {
SharedPreferenceUtil.setValue(it, "X-Token", "")
}
}