Android-使用OKHTTP Interceptor刷新Access-Token

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

Android-使用OKHTTP Interceptor刷新Access-Token

    • Android-使用OKHTTP Interceptor刷新Access-Token
    • 前言
    • 使用OKhttp Tnterceptor 刷新 Token
      • Interceptor 是什么
      • 如何使用拦截器
        • 添加 Application Interceptor 拦截器
        • 添加 Network Interceptor 拦截器
        • 添加的拦截器是什么时候执行的?
      • 自定义 Interceptor 刷新 Token
        • 实现刷新Token的Interceptor
          • 取出Response
          • 预处理返回
          • 设置接口白名单
          • 和本地Token进行对比。
          • 读取返回数据并判断Token是否失效
          • 使用新Token重新请求接口
        • 测试使用
      • 总结
      • 参考

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 是什么

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

  • Application Interceptor
  • Network Interceptor

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

Android-使用OKHTTP Interceptor刷新Access-Token_第1张图片

如何使用拦截器

添加 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() 如下:

Android-使用OKHTTP Interceptor刷新Access-Token_第2张图片

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

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

    上面添加Network 级 Interceptor 受 forWebSocket 变量控制,不过在默认情况下使用 forWebSocket 这个变量都是 false
    Android-使用OKHTTP Interceptor刷新Access-Token_第3张图片

自定义 Interceptor 刷新 Token

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

实现刷新Token的Interceptor

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

/**
 * Created by shishoufeng on 2019/4/16.
 * 

* desc : token 拦截器 用于校验 token 是否失效 如果失效需要 及时更新 token *

*/ 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.
 * 

* 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();

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

Android-使用OKHTTP Interceptor刷新Access-Token_第4张图片

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

总结

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

优点:

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

缺点:

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

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

参考

https://github.com/square/okhttp

你可能感兴趣的:(Android基础,android)