Android-使用OKHTTP Interceptor刷新Access-Token

本文介绍了如何在Android应用中利用OKHTTP的Interceptor自动刷新Access Token,以提升用户体验。通过创建自定义Interceptor,判断接口返回的Token是否失效,并在失效时使用Refresh Token请求新的Access Token,然后重新发送请求。文中详细阐述了Interceptor的工作原理、如何添加Interceptor以及其实现过程,并分析了这种方法的优缺点。

本blog文章为ShiShouFeng原创文章,如需转载引用请注明出处,谢谢
https://blog.youkuaiyun.com/ForwardSailing/article/details/106449790

Android-使用OKHTTP Interceptor刷新Access-Token

前言

Token设计

一般软件设计为保证安全服务器端引入Token令牌校验,客户端在发起网络请求时,需要在请求头中携带 Token 令牌信息,服务端对 Token 令牌进行校验,校验 Token合法且 Token超过有效期 服务端会返回相应数据,如果不合法 或 Token失效情况下会 HTTP Status 返回 401 或 接口返回信息中 code 相应错误码 (我们以返回 -10001为例)

为提高用户体验,避免因 AccessToken 失效而导致用户重新登录,所以客户端需要在 AccessToken 失效后 使用 Refresh Token 请求刷新Token接口,改接口会重新返回一个 AccessToken 、RefreshToken 等信息。

注意:客户端一般情况需要把接口返回信息保存在本地

现在考虑有如下业务场景:

用户想要获取 商品信息(/get/shop/details.v1.0) 但是接口返回Token失效,此时需要用刷新Token (/refresh/token.v1.0) 接口获取新的AccessToken,之后将新的Token添加到 获取商品信息接口中,之后再重新请求接口。

在上面的业务场景中,如果使用OKhttp 的 Interceptor 拦截器实现就比较简单了,下面看一下具体该怎么做。

使用OKhttp Tnterceptor 刷新 Token

Interceptor 是什么

InterceptorOKhttp 中 利用责任链设计模式设计的、链式请求和返回结果的链式调用,Interceptor 分为两种责任链,分别是:

  • Application Interceptor
  • Network Interceptor

这两者本身实现上并没有差别,唯一的区别是 工作的层次不同,Application 级 Interceptor 会首先执行,返回结果后最后执行,而 Network 级 Interceptor 是工作在 发起网络请求前执行,如下图所示:

在这里插入图片描述

如何使用拦截器

添加 Application Interceptor 拦截器

添加 Application Interceptor 拦截器 只需要获取 OKHttpClient 对象 调用 addInterceptor 方法即可

public Builder addInterceptor(Interceptor interceptor) {
  if (interceptor == null) throw new IllegalArgumentException("interceptor == null");
  interceptors.add(interceptor);
  return this;
}
添加 Network Interceptor 拦截器

添加 Network Interceptor 拦截器 只需要获取 OKHttpClient 对象 调用 addNetworkInterceptor 方法即可

public Builder addNetworkInterceptor(Interceptor interceptor) {
  if (interceptor == null) throw new IllegalArgumentException("interceptor == null");
  networkInterceptors.add(interceptor);
  return this;
}
添加的拦截器是什么时候执行的?

上面我们知道了两种拦截器添加方式,那么添加的拦截器是什么时候执行的呢?添加的拦截器最终会调用到 RealCall 类中的 getResponseWithInterceptorChain() 如下:

在这里插入图片描述

  • ①处:Application 级别 Interceptor 在每次执行网络请求时 就添加至 责任链中

  • ②处:在网络真正发起前将 Network 级 Interceptor 添加至拦截器中

    上面添加Network 级 Interceptor 受 forWebSocket 变量控制,不过在默认情况下使用 forWebSocket 这个变量都是 false
    在这里插入图片描述

自定义 Interceptor 刷新 Token

前面我们 我们知道了 Interceptor 是什么以及如何使用 Interceptor 、接下来我们就利用 Interceptor 来实现我们的业务:

实现刷新Token的Interceptor

我们创建TokenInterceptor 并实现intercept() 实现以下逻辑:

/**
 * Created by shishoufeng on 2019/4/16.
 * <p>
 * desc :  token 拦截器 用于校验 token 是否失效 如果失效需要 及时更新 token
 * <p>
 */
public class TokenInterceptor implements Interceptor{

    private static final String TAG = "TokenInterceptor";

    private static final Charset UTF8 = Charset.forName("UTF-8");

    private Context mContext;

    private OkHttpClient mOkHttpClient;

    private static final Object LOCK_OBJ = new Object();

    public TokenInterceptor(Context context) {
        this.mContext = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request originRequest = chain.request();
        //step1: 拿到原请求结果
        Response originResponse = chain.proceed(originRequest);

        //原请求结果为空 直接返回结果
        if (originResponse == null) {
            return null;
        }
        // 原请求地址
        String originReqUrl = originRequest.url().toString();

        //step2: 请求头中不含 token 直接返回原请求结果
        String originReqAccessToken = originRequest.header("Authorization");
        if (StringUtil.isEmpty(originReqAccessToken)) {    //请求头中不含 token 直接返回原请求结果
            return originResponse;
        }

        //step3: 判断是否在 拦截白名单中 如果在直接返回 原请求结果
        if (InterceptorConfigUtils.isWhiteUrl(originReqUrl)){ 
            return originResponse;
        }
		
		//进行同步处理防止多线程并发场景下造成重复刷新
        synchronized (LOCK_OBJ) {
            String localAccessToken = LoginUtils.getAccessToken();

            //step4: 如果 请求token 和 本地token不一致 直接使用 本地token 进行请求
            if (!TextUtils.equals(originReqAccessToken, localAccessToken)) {
                // 重新构建请求
                Request newRequest = originRequest.newBuilder()
                        .header("Authorization", localAccessToken)
                        .build();
                // 重新发起请求
                return chain.proceed(newRequest);
            }

            // 拿到返回结果
            ResponseBody responseBody = originResponse.body();
            if (responseBody == null) {     //返回结果为空 直接返回
                return originResponse;
            }
			
			//step5:读取返回数据并判断token是否失效、如果失效执行刷新操作
            // 设置编码 准备读取返回数据
            Charset charset = UTF8;
            BufferedSource bufferedSource = responseBody.source();
            bufferedSource.request(Long.MAX_VALUE);
            Buffer buffer = bufferedSource.buffer();

            MediaType contentType = responseBody.contentType();
            if (contentType != null) {
                charset = contentType.charset(UTF8);
            }
            //编码为空 直接返回 
            if (charset == null) {
                return originResponse;
            }
            // 网络请求返回数据
            String bodyString = buffer.clone().readString(charset);

            if (isTokenOverdue(bodyString)) {    // token 已过期

                //step6: 请求 token 并保存结果
                boolean isRefreshTokenOk = requestTokenAndSaveData();

                if (isRefreshTokenOk) {  // 更新token成功

                    // 获取原 请求头信息
                    Headers originHeaders = originRequest.headers();
                    // 如果请求头中含有 token  请求字段 重新添加更新后的token
                    if (originHeaders != null && originHeaders.names().contains("Authorization")) {

                        // 构建新的请求头
                        Headers newHeaders = originHeaders.newBuilder()
                                .set("Authorization", LoginUtils.getAccessToken())
                                .build();
                        // 重新构建请求
                        Request newRequest = originRequest.newBuilder()
                                .headers(newHeaders)
                                .build();
                        // 重新发起请求
                        return chain.proceed(newRequest);
                    }
                    // 重新发起请求
                    return chain.proceed(originRequest);
                }
            }

            // 没有更新 token 成功 直接返回原结果
            return originResponse;
        }
    }
}

上述代码虽然比较长但是每一步都有详细的注释,所以理解起来不会太费劲,下面我详细说一下每一步的意思:

取出Response

step1:首先要使用 Chain调用其 proceed() 完成整个网络请求,得到Response

预处理返回

step2:预处理返回。对接口请求参数中不含有Token的进行跳过提高性能

设置接口白名单

step3:接口免检查白名单。对于有些接口不需要拦截和检查则可以设立一个拦截白名单,如果在直接返回 原请求结果

这一步根据自己的需求而定,不是必须的

这里我直接是一个Set集合进行过滤的:

/**
 * Created by shishoufeng on 2019/6/13.
 * <p>
 * desc : 拦截器配置类
 */
class InterceptorConfigUtils {
    // 请求白名单
    private static Set<String> whiteUrlSet = new HashSet<>(3);
    // 添加自己的白名单接口
    static {
        whiteUrlSet.add("https://your api url ");     
      	//...
    }
    /**
     *
     * 判断 指定请求的URL 是否在 本地白名单中
     *
     * @param reqUrl 请求URL
     * @return true 在白名单中 不需要拦截处理 false 不在配置集合中 需要进行拦截处理
     */
    static boolean isWhiteUrl(String reqUrl){
        return !ArrayUtils.isEmpty(whiteUrlSet) && whiteUrlSet.contains(reqUrl);
    }
}
和本地Token进行对比。

step4: 和本地Token进行对比、如果走到这里说明此接口的Token有可能已经失效、所以这里将请求Token 和 本地Token对比、如果不一致 直接使用 本地Token 进行请求。
为什么这样处理呢?是因为有可能在这个接口之前可能已经有接口刷新完Token并将最新结果保存到本地了,所以这里没有必要再去刷新Token

读取返回数据并判断Token是否失效

step5: 读取返回数据并判断Token是否失效、如果失效执行刷新操作,这一步骤是比较重要

requestTokenAndSaveData() 源码如下:

/**
 * 请求token 并将token 保存到 本地
 *
 * @return true 成功 false 失败
 * @throws IOException
 */
private boolean requestTokenAndSaveData() throws IOException {
    Request tokenRequest;
    Request.Builder reqBuilder;
    // 刷新token 地址
    String requestUrl = HostConfig.getHostConfig().getApiHost() + ServerAdr.TokenConst.refreshToken;

    reqBuilder = new Request.Builder();
    reqBuilder.url(requestUrl);
    // 使用 refresh_token 刷新 access_token
    reqBuilder.addHeader(LoginConstant.ACCESS_TOKEN_KEY, UserDataManager.getInstance().getLoginBean().getRefreshToken());
    // 构建 body 请求体
    RequestBody reqBody = RequestBody.create(Constant.HttpParamConstant.APPLICATION_JSON_TYPE, new JSONObject().toString());
    // post 方式发送 并构建 request
    tokenRequest = reqBuilder.post(reqBody)
            .build();
    // 同步请求 token
    Response response = getOkHttpClient().newCall(tokenRequest)
            .execute();
    // 处理返回结果
    try {
        if (!response.isSuccessful()) {
            return false;
        }
        ResponseBody body = response.body();
        if (body == null) {
            return false;
        }
        String resultData = body.string();
        if (StringUtil.isEmpty(resultData)) {
            return false;
        }
        // 解析数据
        JSONObject jo = JSON.parseObject(resultData);
        int code = DataParserUtil.getJsonInt(jo, Net.Field.code);
        if ( code != Net.HttpErrorCode.SUCCESS) {
            return false;
        }
        // 解析body
       JSONObject data = DataParserUtil.getJsonObj(jo, Net.Field.body);
        if (data == null) {
            return false;
        }
        // 解析对象
        UpdateTokenBean tokenBean = DataParserUtil.parseObject(data.toString(), UpdateTokenBean.class);
        
        if (tokenBean == null || StringUtil.isEmpty(tokenBean.getAccessToken()) || StringUtil.isEmpty(tokenBean.getRefreshToken())) {
            return false;
        }
        // 更新token
        UserInfoUtils.saveTokenInfo(tokenBean);
        return true;
    } finally {
        CloseUtils.close(response);
    }
}
使用新Token重新请求接口

step6:在前面的操作中我们成功刷新了Token,并保存到本地,这个时候要利用 OKhttp 的拦截器优势,重新构建一个 request 再次调用 proceed() 重新发送一次请求来达到不丢掉本次请求的功能。

关键代码如下:

// 构建新的请求头
Headers newHeaders = originHeaders.newBuilder()
        .set(LoginConstant.ACCESS_TOKEN_KEY, LoginUtils.getAccessToken())
        .build();
// 重新构建请求
Request newRequest = originRequest.newBuilder()
        .headers(newHeaders)
        .build();
// 重新发起请求
return chain.proceed(newRequest);

至此我们的Token拦截器工作基本结束了,下面就看一下能不能正常使用。

测试使用

最后将我们的拦截器加入到 OkHttpClient 中

mOkClient = getOkHttpBuilder(context)
        .addInterceptor(new TokenInterceptor(getApplicationContext())
        .build();

最后就可以根据自己的业务场景进行测试了,经过大量的并发测试,使用抓包工具查看如下结果:

在这里插入图片描述

我们看到 ①处同一时间会有 三个接口Token失效了,而②处刷新Token 只会请求一次,在刷新完Token之后,又重新将token失效的接口重新请求了一次③处

总结

至此我们使用OKhttp Tnterceptor 刷新 Token 的工作和流程已经清楚了,我们来总结一下:

优点:

  1. 使用方便、代码可维护性强、对业务代码没有侵入性。
  2. 高效、安全。能够做到对全部接口进行全局处理

缺点:

  1. 由于拦截器基于 责任链设计模式 设计 对数据改造和拓展很灵活 但 对性能有稍微影响

    经过统计会在原来的接口请求基础上慢一些! 3 ~ 20 毫秒的影响! 不过在灵活性、代码维护性、代码可侵入性上 这个代价还是可以接受的

参考

https://github.com/square/okhttp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值