RxJava+Retrofit之token自动刷新(二)

上篇文章主要对Retrofit做了封装,使之使用起来更加方便。在之前的封装中token过期再次刷新token后需要手动调用之前的请求,这种处理方式不够优雅,因此,在原有的基础上,本篇文章将基于上篇文章的封装并优化Token验证机制。使之能够实现过期自动刷新并重新调用请求。
接下来将通过以下几个小节来学习如何实现token验证。

  • 为什么要引入token机制
  • token机制的验证流程
  • RxJava+Retrofit封装实现token验证

一、为什么引入token机制

1.token是什么?

token意为令牌,通常是由客户端携带IMEI/Mac到服务器,服务器根据客户端的IMEI/Mac生成一段字符串并返回给客户端,并为其设置有效期。以此作为客户端和服务端交互的令牌。客户端每次请求都会携带token到服务器来代替用户名和密码。服务端验证token有效后则返回数据给客户端,否则返回特定的错误码给客户端。客户端根据错误码去做相应的处理。

2.那么为什么引入token机制呢?

主要有以下两个原因:
(1)保证安全性。如果不引入token机制,那么我们每次请求数据都要携带用户名和密码。也就是每次请求数据用户名和密码都会在网络上传输。这样大大增加了安全风险,很容易被黑客截取。因此引入token机制也一定程度上保证了安全性。
(2)减小服务器压力。在引入token机制前,我们需要通过用户名和密码到服务器去验证用户身份是否合法。服务器认证用户名和密码是一个查询操作,如果用户量大,那么就会相应增加服务器的压力。而引入token机制后,服务器就可以将token作为一个用户的唯一标识来验证用户身份是否合法。这样可以大大减少服务器的压力。

二、token机制的验证流程

token的验证流程并非唯一的,至于使用怎样的验证流程可以自行确定。本文中采用OAuth2.0协议实现token验证机制。
主要步骤如下:

  1. 通过用户名和密码登录成功获取token和refreshToken并保存到本地。
  2. token的有效期为2小时,refreshToken的有效期为15天。
  3. 每次网络请求都需要带上token,而不必带上refreshToken。
  4. 如果服务器端判断token过期,则返回对应的错误码,客户端判断错误码后调用刷新token接口,重新获取token和refreshToken并存储。
  5. 如果连续15天未使用app或者用户修改了密码,则refreshToken过期,需要重新登录获取token和refreshToken。

三、RxJava+Retrofit封装实现token自动刷新

有了以上两节的基础,我们就可以来自己实现token机制的验证了。在这里我们使用上篇文章中封装的RxJava和Retrofit来实现token机制。

1.登录认证,获取token和refresh_token

登录时我们需要两个参数:用户名username、密码password以及appkey作为一个唯一id,每次登录成功服务器会返回token和refreshToken。登录请求的实体类LoginRequest如下:

1
2
3
4
public class LoginRequest extends BaseRequest{
private String userId;
private String password;
}

接下来我们就可以来调用登录接口获取token了。登录成功后我们可将token和refreshToekn存储到本地。以提交表单为例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void login() {
Map<String, Object> map = MapUtils.entityToMap(new BaseRequest());
map.put("userId","123456");
map.put("password","123123");
IdeaApi.getApiService()
.login(map)
.subscribeOn(Schedulers.io())
.compose(activity.<BasicResponse<LoginResponse>>bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<BasicResponse<LoginResponse>>(activity) {
@Override
public void onSuccess(BasicResponse<LoginResponse> response) {
LoginResponse results = response.getResults();
ToastUtils.show("登录成功!获取到token" + results.getToken() + ",可以存储到本地了");
/**
* 可以将这些数据存储到User中,User存储到本地数据库
*/
SharedPreferencesHelper.put(activity, "token", results.getToken());
SharedPreferencesHelper.put(activity, "refresh_token", results.getRefresh_token());
SharedPreferencesHelper.put(activity, "refresh_secret", results.getRefresh_secret());
}
});
}

2.明确需求,抛出异常

由于token的有效期较短,因此我们需要经常刷新token来保证token的有效性。在请求网络的时候如果token过期或者无效服务器会给我们返回对应的错误码。我们需要根据状态码来判断token是否失效。如果失效则调用刷新token接口重新获取token。如果refreshToekn也过期那么我们需要重新登录。

现在,我们的需求是要实现token过期后自动刷新,刷新成功后自动调用原来的请求,如果refreshToken也过期,则退出登录。基于此,我们可以联想到RxJava的retryWhen操作符,我们可以通过retryWhen操作符判断token过期并自动刷新。

那么,接下来我们首要任务是如何判断token和refreshToken过期。还记得上篇文章中我们修改GsonResponseBodyConverter类来根据后台响应码来获取data中的数据。显然在此处判断token是否过期是比较合适的。接下来看GsonResponseBodyConverter中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public Object convert(ResponseBody value) throws IOException {
try {
BasicResponse response = (BasicResponse) adapter.fromJson(value.charStream());
if (response.getCode() == SUCCESS) {
if (response.getData() == null)
throw new ServerNoDataException(0, "");
return response.getData();
} else if (response.getCode() == TOKEN_EXPIRED) {
throw new TokenExpiredException(response.getCode(), response.getMessage());
} else if (response.getCode() == REFRESH_TOKEN_EXPIRED) {
throw new RefreshTokenExpiredException(response.getCode(), response.getMessage());
} else if (response.getCode() != SUCCESS) {
// 特定 API 的错误,在相应的 DefaultObserver 的 onError 的方法中进行处理
throw new ServerResponseException(response.getCode(), response.getMessage());
}
} finally {
value.close();
}
return null;
}

上面代码中我们自定义了几个异常,在判断对应的错误码后抛出对应的异常。此处我们可以着重关心下TokenExpiredException和RefreshTokenExpiredException,分别代表了token过期和refreshToken过期。

3.添加代理,实现token过期自动刷新

因为几乎所有的请求都需要验证token是否过期,因此需要做统一处理。我们可以采用代理类来对Retrofit的API做统一的代理处理。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IdeaApiProxy implements IGlobalManager {
@SuppressWarnings("unchecked")
public <T> T getApiService(Class<T> tClass,String baseUrl) {
T t = RetrofitService.getRetrofitBuilder(baseUrl)
.build().create(tClass);
return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class<?>[] { tClass }, new ProxyHandler(t, this));
}

@Override
public void exitLogin() {

}
}

这样,我们就需要通过IdeaApiProxy 中的getApiService方法来创建API请求。其中的ProxyHandler则是实现了InvocationHandler。ProxyHandler类是我们处理token自动刷新的核心类。思想就是针对 method 的调用,做以 retryWhen 的包装,在retryWhen 中获取相应的异常信息来做处理,看 retryWhen 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
return Observable.just(true).flatMap(new Function<Object, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Object o) throws Exception {
try {
try {
if (mIsTokenNeedRefresh) {
updateMethodToken(method, args);
}
return (Observable<?>) method.invoke(mProxyObject, args);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Observable<Throwable> observable) throws Exception {
return observable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
if (throwable instanceof TokenExpiredException) {// token过期
return refreshTokenWhenTokenInvalid();
} else if (throwable instanceof RefreshTokenExpiredException) {
// RefreshToken过期,执行退出登录的操作。
mGlobalManager.logout();
return Observable.error(throwable);
}
return Observable.error(throwable);
}
});
}
});
}

这里针对 token 过期的 TokenExpiredException的异常,执行刷新 token 的操作,刷新 token 的操作则是直接调用 Retrofit 的方法,而不需要走代理了。另外它必须是个同步的代码块,一起来看refreshTokenWhenTokenInvalid方法中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private Observable<?> refreshTokenWhenTokenInvalid() {
synchronized (ProxyHandler.class) {
// Have refreshed the token successfully in the valid time.
if (new Date().getTime() - tokenChangedTime < REFRESH_TOKEN_VALID_TIME) { // 防止重复刷新token
mIsTokenNeedRefresh = true;
return Observable.just(true);
} else {
Map<String, Object> map = MapUtils.entityToMap(new BaseRequestData());
RetrofitHelper.getApiService()
.refreshToken(map)
//.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<RefreshTokenResponse>() {
@Override
public void onSuccess(RefreshTokenResponse response) {
if (response != null) {
// 保存数据到本地
mGlobalManager.tokenRefresh(response);
mIsTokenNeedRefresh = true;
tokenChangedTime = new Date().getTime();
}
}

@Override
public void onError(Throwable e) {
super.onError(e);
mRefreshTokenError = e;
}
});
if (mRefreshTokenError != null) {
return Observable.error(mRefreshTokenError);
} else {
return Observable.just(true);
}
}
}
}

4.刷新token成功后替换旧的token

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void updateMethodToken(Method method, Object[] args) {
ServerKey serverKey = RealmDatabaseHelper.queryFirstFrom(ServerKey.class);
String token = serverKey.getToken();
if (mIsTokenNeedRefresh && !TextUtils.isEmpty(token)) {
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 FieldMap||annotation instanceof QueryMap) {// 以Map方式提交表单
if (args[i] instanceof Map)
((Map<String, Object>) args[i]).put(TOKEN, token);
} else if (annotation instanceof Query) {
if (TOKEN.equals(((Query) annotation).value())) {
args[i] = token;
}
} else if (annotation instanceof Field) {
if (TOKEN.equals(((Field) annotation).value())) {
args[i] = token;
}
}else if(annotation instanceof Part){ // 上传文件
if (TOKEN.equals(((Part) annotation).value())) {
RequestBody tokenBody = RequestBody.create(MediaType.parse("multipart/form-data"), token);
args[i] = tokenBody;
}
}else if(annotation instanceof Body){// Post提交json数据
if(args[i] instanceof BaseRequest){
BaseRequest requestData= (BaseRequest) args[i];
requestData.setToken(token);
args[i]=requestData;
}
}
}
}
}
mIsTokenNeedRefresh = false;
}
}

这里我们遍历所有的token字段,并将其替换成新的token。但上述方法仅仅适用于get请求和post请求以表单格式提交。如果是post请求且提交格式为json可以自行添加。另外此种方法不适用于token放在请求头的方式。

(一)Rxjava2+Retrofit完美封装
(二)Rxjava2+Retrofit之Token自动刷新
(三)Rxjava2+Retrofit实现文件上传与下载

参考
RxJava+Retrofit实现全局过期token自动刷新Demo篇

源码下载


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!