Retrofit Token过期自动刷新并重新请求接口
来源:互联网 发布:怎样做网络宣传 编辑:程序博客网 时间:2024/05/18 16:16
在有心课堂的群里,有网友提出如下场景:
当前开发的 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 的方法:
@Overridepublic 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方法:
@Overridepublic 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
- Retrofit Token过期自动刷新并重新请求接口
- RxJava+Retrofit实现全局过期token自动刷新的实践
- Rxjava+Retrofit 实现全局过期 Token 自动刷新
- 基于retrofit网络请求token过期的处理逻辑
- 请求加密,响应数据解密,过期自动刷新并且重新请求接口
- retrofit 刷新token并发处理
- 使用RxJava的retryWhen操作符实现token过期自动刷新
- Java 华为推送 Access Token过期刷新
- Tomcat 自动启动并重新加载应用
- token失效后再次请求获取新token
- retrofit网络请求地址接口的拼接
- Android Token过期解决方案
- 强制Gradle/Maven刷新缓存并重新从Nexus下载依赖jar包
- retrofit+rxjava+recyclerview+下拉刷新+自动加载更多
- retrofit请求
- Retrofit 请求
- 使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求
- 使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求
- UVA-11080 Place the Guards(二分图染色)
- Java中堆内存和栈内存详解
- 011/12/20 令人眩晕的RS232 DB9 公母头和交叉直连
- 不用sdk实现分享多张本地图片功能
- Redis与Memcached的区别
- Retrofit Token过期自动刷新并重新请求接口
- 阿里秒级android增量编译工具freeLine的使用入门
- Url参数中出现+、空格、=、%、&、#等字符的解决办法
- 关于RPC
- tensorflow compile
- java设计模式进阶_intercepting-filter
- Spring连接多个数据源
- 搭建一个android项目的准备工作
- IntelliJ IDEA+JetBrains WebStorm )+JetBrains PhpStorm +JetBrains(2016.2)版本的破解方法总结