retrofit+okhttp实现单点登录自动刷新token并处理并发问题

retrofit+okhttp实现单点登录自动刷新token

说明

在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", "")
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值