Retrofit Token过期自动刷新并重新请求接口

在有心课堂的群里,有网友提出如下场景:

当前开发的 App 遇到一个问题:

当请求某个接口时,由于 token 已经失效,所以接口会报错。

但是产品经理希望 app 能够马上刷新 token ,然后重复请求刚才那个接口,这个过程对用户来说是无感的。

请求 A 接口-》服务器返回 token 过期-》请求 token 刷新接口-》请求 A 接口

我们应该是怎么解决这个问题呢?

经过百度搜索到了相关信息,这里总结下。

本文是采用RxJava + Retrofit来实现网络请求封装。

实现原理

利用 Observale 的 retryWhen 的方法,识别 token 过期失效的错误信息,此时发出刷新 token 请求的代码块,完成之后更新 token,这时之前的请求会重新执行,但将它的 token 更新为最新的。另外通过代理类对所有的请求都进行处理,完成之后,我们只需关注单个 API 的实现,而不用每个都考虑 token 过期,大大地实现解耦操作。

App多个请求token失效的处理逻辑

当集成了Retrofit之后,我们app中的网络请求接口则变成了一个个单独的方法,这时我们需要添加一个全局的token错误抛出机制,来避免每个接口都所需要的token验证处理。

token失效错误抛出

在Retrofit中的Builder中,是通过GsonConvertFactory来做json转成model数据处理的,这里我们就需要重新实现一个自己的GsonConvertFactory,这里主要由三个文件GsonConvertFactory,GsonRequestBodyConverter,GsonResponseBodyConverter,它们三个从源码中拿过来新建即可。主要我们重写GsonResponseBodyConverter这个类中的convert的方法,这个方法主要将ResponseBody转换我们需要的Object,这里我们通过拿到我们的token失效的错误信息,然后将其以一个指定的Exception的信息抛出。

GsonConverterFactory代码如下:

修改的地方:

1.修改 GsonConverterFactory 中,生成 GsonResponseBodyConverter 的方法:

@Override
public Converter<ResponseBody, ?> responseBodyConverter(final Type type, Annotation[] annotations, Retrofit retrofit) {
  Type newType = new ParameterizedType() {
      @Override
      public Type[] getActualTypeArguments() {
          return new Type[] { type };
      }

      @Override
      public Type getOwnerType() {
          return null;
      }

      @Override
      public Type getRawType() {
          return ApiModel.class;
      }
  };
  TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(newType));
  return new GsonResponseBodyConverter<>(adapter);
}

可以看出我们这里对 type 类型,做以包装,让其重新生成一个类型为 ApiModel 的新类型。因为我们在写接口代码的时候,都以真正的类型 type 来作为返回值的,而不是 ApiModel。

2.GsonResponseBodyConverter的处理 它的修改,则是要针对返回结果,做以异常的判断并抛出,主要看其的 convert方法:

@Override
public Object convert(ResponseBody value) throws IOException {
  try {
      ApiModel apiModel = (ApiModel) adapter.fromJson(value.charStream());
      if (apiModel.errorCode == ErrorCode.TOKEN_NOT_EXIST) {
          throw new TokenNotExistException();
      } else if (apiModel.errorCode == ErrorCode.TOKEN_INVALID) {
          throw new TokenInvalidException();
      } else if (!apiModel.success) {
          // TODO: 16/8/21 handle the other error.
          return null;
      } else if (apiModel.success) {
          return apiModel.data;
      }
  } finally {
      value.close();
  }
  return null;
}

错误抛出

当服务器错误信息的时候,同样也是一个 model,不同的是 success 为 false,并且含有 error_code的信息。所以我们需要针对 model 处理的时候,做以判断。主要修改的地方就是 retrofit 的 GsonConvertFactory,这里不再通过 gradle 引入,直接把其源码中的三个文件添加到咱们的项目中。

首先提及的一下是对统一 model 的封装,如下:

public class ApiModel<T> {
    public boolean success;
    @SerializedName("error_code") public int errorCode;

    public T data;
}

当正确返回的时候,我们获取到 data,直接给上层;当出错的时候,可以针对 errorCode的信息,做一些处理,让其走最上层调用的 onError 方法。

多请求的API代理

为所有的请求都添加Token的错误验证,还要做统一的处理。借鉴Retrofit创建接口的api,我们也采用代理类,来对Retrofit的API做统一的代理处理。

建立API代理类

public class ApiServiceProxy {

    Retrofit mRetrofit;

    ProxyHandler mProxyHandler;

    public ApiServiceProxy(Retrofit retrofit, ProxyHandler proxyHandler) {
        mRetrofit = retrofit;
        mProxyHandler = proxyHandler;
    }

    public <T> T getProxy(Class<T> tClass) {
        T t = mRetrofit.create(tClass);
        mProxyHandler.setObject(t);
        return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class<?>[] { tClass }, mProxyHandler);
    }
}

这样,我们就需要通过ApiServiceProxy中的getProxy方法来创建API请求。另外,其中的ProxyHandler则是实现InvocationHandler来实现。

public class ProxyHandler implements InvocationHandler {

    private Object mObject;

    public void setObject(Object obj) {
        this.mObject = obj;
    }

    @Override
    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
        Object result = null;
        result = Observable.just(null)
            .flatMap(new Func1<Object, Observable<?>>() {
                @Override
                public Observable<?> call(Object o) {
                    try {
                        checkTokenValid(method, args);
                        return (Observable<?>) method.invoke(mObject, args);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    return Observable.just(new APIException(-100, "method call error"));
                }
            }).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                             @Override
                             public Observable<?> call(Observable<? extends Throwable> observable) {
                                 return observable.
                                     flatMap(new Func1<Throwable, Observable<?>>() {
                                                 @Override
                                                 public Observable<?> call(Throwable throwable) {
                                                     Observable<?> x = checkApiError(throwable);
                                                     if (x != null) return x;
                                                     return Observable.error(throwable);
                                                 }
                                             }

                                     );
                             }
                         }

                , Schedulers.trampoline());
        return result;
        }
  }

这里的invoke方法则是我们的重头戏,在其中通过将method.invoke方法包装在Observable中,并添加retryWhen的方法,在retryWhen方法中,则对我们在GsonResponseBodyConverter中暴露出来的错误,做一判断,然后执行重新获取token的操作,这段代码就很简单了。就不再这里细述了。

还有一个重要的地方就是,当token刷新成功之后,我们将旧的token替换掉呢?笔者查了一下,java8中的method类,已经支持了动态获取方法名称,而之前的Java版本则是不支持的。那这里怎么办呢?通过看retrofit的调用,可以知道retrofit是可以将接口中的方法转换成API请求,并需要封装参数的。那就需要看一下Retrofit是如何实现的呢?最后发现重头戏是在Retrofit对每个方法添加的@interface的注解,通过Method类中的getParameterAnnotations来进行获取,主要的代码实现如下:

/**
     * Update the token of the args in the method.
     */
private void updateMethodToken(Method method, Object[] args) {
        if (mIsTokenNeedRefresh && !TextUtils.isEmpty(GlobalToken.getToken())) {
            Annotation[][] annotationsArray = method.getParameterAnnotations();
            Annotation[] annotations;
            if (annotationsArray != null && annotationsArray.length > 0) {
                for (int i = 0; i < annotationsArray.length; i++) {
                    annotations = annotationsArray[i];
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Query) {
                            if (TOKEN.equals(((Query) annotation).value())) {
                                args[i] = GlobalToken.getToken();
                            }
                        }
                    }
                }
            }
            mIsTokenNeedRefresh = false;
        }
    }

这里,则遍历我们所使用的token字段,然后将其替换成新的token.

代码验证

最上层的代码调用中,添加了两个按钮:

按钮1:获取token

token 获取成功之后,仅仅更新一下全局的token即可。

按钮2:正常的请求

这里为了模拟多请求,这里我直接调正常的请求5次:

为了查看输出,另外对 Okhttp 添加了 HttpLoggingInterceptor 并设置 Body 的 level 输出,用来监测 http 请求的输出。

一切完成之后,先点击获取 token 的按钮,等待30秒之后,再点击正常请求按钮。可以看到如下的输出:

--> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (8ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (5ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 --> GET http://192.168.56.1:8888/refresh_token http/1.1
 --> END GET
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 {"success":false,"error_code":1001}
 Transfer-Encoding: chunked
 <-- END HTTP (35-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/refresh_token (2ms)
 Content-Type: text/plain
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (6ms)
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Connection: keep-alive
 Transfer-Encoding: chunked
 Transfer-Encoding: chunked
 {"success":true,"data":{"token":"1471826289336"}}
 <-- END HTTP (49-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
roxy: Refresh token success, time = 1471790019657
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (2ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (6ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)

刚发出的5个请求都返回了 token 过期的 error,之后看到一个重新刷新 token 的请求,它成功之后,原先的5个请求又进行了重试,并都返回了成功的信息。

完整代码:

https://github.com/alighters/AndroidDemos/tree/master/app/src/main/java/com/lighters/demos/token

server代码则是根目录下的 server 文件夹中,测试的时候不要忘启动 server 哦。

参考文章:

http://alighters.com/blog/2016/08/22/rxjava-plus-retrofitshi-xian-zhi-demo/



以上实现是将token放在在url里面,如果是放在Header里面,怎么实现呢?还是要通过okhttp的拦截器来实现。

思路:

1.通过拦截器,获取返回的数据

2.判断token是否过期

3.如果token过期则刷新token

4.使用最新的token,重新请求网络数据

实现如下:

public class TokenInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        if (isTokenExpired(response)) {//根据和服务端的约定判断token过期
            //同步请求方式,获取最新的Token
            String newSession = getNewToken();
            //使用新的Token,创建新的请求
            Request newRequest = chain.request()
                    .newBuilder()
                    .header("Cookie", "JSESSIONID=" + newSession)
                    .build();
            //重新请求
            return chain.proceed(newRequest);
        }
        return response;
    }

    /**
     * 根据Response,判断Token是否失效
     *
     * @param response
     * @return
     */
    private boolean isTokenExpired(Response response) {
        if (response.code() == 404) {
            return true;
        }
        return false;
    }

    /**
     * 同步请求方式,获取最新的Token
     *
     * @return
     */
    private String getNewToken() throws IOException {
        // 通过一个特定的接口获取新的token,此处要用到同步的retrofit请求
        Response_Login loginInfo = CacheManager.restoreLoginInfo(BaseApplication.getContext());
        String username = loginInfo.getUserName();
        String password = loginInfo.getPassword();

        Call<Response_Login> call = WebHelper.getSyncInterface().synclogin(new Request_Login(username, password));
        loginInfo = call.execute().body();

        loginInfo.setPassword(password);
        CacheManager.saveLoginInfo(loginInfo);
        return loginInfo.getSession();
    }
}

添加拦截器:

OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new TokenInterceptor())
                .build();

参考:http://www.jianshu.com/p/8d1ee61bc2d2

时间: 2024-10-15 15:46:24

Retrofit Token过期自动刷新并重新请求接口的相关文章

axios reponse请求拦截以及token过期跳转问题

前两天项目中遇到了token拦截,需要在请求的header头里放置token,需要用到response拦截,调试过程中遇到了拿不到token的问题 我用的axios实例 let token = store.state.token instance.interceptors.request.use(config => { // 在发送请求之前做些什么 //判断是否存在token,如果存在将每个页面header都添加token config.headers['Content-Type'] = 'ap

SpringCache自定义过期时间及自动刷新

背景前提 阅读说明(十分重要) 对于Cache和SpringCache原理不太清楚的朋友,可以看我之前写的文章:Springboot中的缓存Cache和CacheManager原理介绍 能关注SpringCache,想了解过期实现和自动刷新的朋友,肯定有一定Java基础的,所以先了解我的思想,达成共识比直接看代码重要许多 你可能只需要我里面其中一个点而不是原搬照抄 我在实现过程遇到三大坑,先跟大家说下,兴许对你有帮助 坑一:自己造轮子 对SpringCache不怎么了解,直接百度缓存看到Redi

页面自动刷新常用方法

在javascript编程中,经常在更新数据之后用到location.reload()实现页面刷新. reload() 方法用于重新加载当前的文档.如果该方法没有规定参数,或者参数是 false,它就会用 HTTP 头 If-Modified-Since 来检测服务器上的文档是否已改变.如果文档已改变,reload() 会再次下载该文档.如果文档未改变,则该方法将从缓存中装载文档.这与用户单击浏览器的刷新按钮的效果是完全一样的. 我们都知道客户端浏览器是有缓存的,里面存放之前访问过的一些网页文件

如何解决前后端token过期问题

问题描述: 首先后端生成的token是有时限的,在一段时间后不管前端用户是否进行了访问后端的操作,后端的token都会过期,在拦截器阶段就会返回错误的请求:token过期,从而拿不到想要的请求数据. 解决思路: 每隔一段时间的后端请求中都将token传送过去获取新的token并返回前端放入cookies中并记录cookie的存储失控,达到更新cookie中token的效果;而长时间不做操作的话我们就可以让他的token失效退出系统了. 解决方式:我们的访问后端的请求都是jQuery的ajax请求

BrowserSync,调试利器--自动刷新(转

---恢复内容开始--- 请想象这样一个场面:你开着两个显示器,一边是IDE里的代码,另一边是浏览器里的你正在开发的应用.此时桌上还放着你的手机,手机里也是这个开发中的应用.然后,你新写了一小段代码,按下了ctrl+s保存.紧接着,你的手机和另一个显示器里的应用,就变成了更新后的效果.你可以马上检查效果是否和你预想的一致,甚至都不需要动一下鼠标... 想起来还不错?嗯,这只是简单地省略掉那个开发过程中会按好多遍的F5刷新. 自动刷新 “自动刷新”并不是新的概念,但对关注“可见”的预览效果的前端开

Viewpager图片自动轮播,网络图片加载,图片自动刷新

package com.teffy.viewpager; import java.util.ArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import android.annotation.SuppressLint; import android.app.Act

4.4 创建自动刷新页面

<1>使用Ajax,用户就可以不必反复点击刷新按钮,而实现网页内容的自动刷新 <2>例程采用单击按钮后开始执行,实际中一般以onload事件代替 <3>setTimeout方法,允许以固定的时间间隔(单位为毫秒)执行给定的方法 <4>createRow()方法使用DOM动态创建内容:refreshTime()用于刷新定时器的值 页面代码: <!DOCTYPE html> <html> <head> <meta cha

打开页面自动刷新网页,自动刷新当前页面,JS调用

reload 方法,该方法强迫浏览器刷新当前页面.语法:location.reload([bForceGet]) 参数: bForceGet, 可选参数, 默认为 false,从客户端缓存里取当前页.true, 则以 GET 方式,从服务端取最新的页面, 相当于客户端点击 F5("刷新") replace 方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,你不能通过"前进"和"后退"来访问已经被替换

JS实现移动端下拉刷新更多分页请求功能方法2.0

本次2.0升级版为js实现移动端加载更多下拉刷新更多分页请求功能方法(数据一次请求,前端分页,适用于数据流量较少,数据量压力小的页面)同时新增loading组件,turnToTop组件. 本文原创非转载,如需转载请注明出处:http://www.cnblogs.com/A-QBlog/p/7068959.html 废话不多说,直接上代码: 1 ;(function (w, $) { 2 3 var loadmore = { 4 /*单页加载更多 通用方法 5 * 6 * @param callb