retrofit 刷新token并发处理

时间:2022-01-20 18:00:18

* 简书地址:http://www.jianshu.com/p/c325f5c32709

背景说明

在app开发中,我们需要保证用户登录之后,如果没有在其他设备上登录,则不需要再次登录,很多都会使用 token 作为安全令牌,开始阶段都会在登录时候获取,一直使用到下次登录。这样的 token 没有什么安全性可言,所以大多app都会做 token 时效性处理。
刷新 token 流程图如下:
retrofit 刷新token并发处理
根据刷新 token 的流程,后台返回以下信息:
* 登录成功,获得 token 信息为:
{"token":"",// token 令牌
"expiresIn":XX(单位秒),// token 有效时间长度
"refreshToken":""//刷新 token 用的参数,只允许使用一次
}

* 后台返回错误 errorCode 说明如下:

rest Code errorCode token RefreshToken 刷新Token 重新登录获取Token
401 4010 token为空 无效
401 4011 token过期 有效
401 4012 token失效 失效
401 4013 token过期 过期
401 4014 token失效 失效
rest Code errorCode 问题说明 解决办法
400 4001 请求参数为空 需要检查逻辑

Android 端对 token 的处理是,在每次用到 token 的时候对 token 是否有效进行判断,获得有效的 token。具体代码如下:

public static Retrofit retrofitClient(String token, String apiUrl) {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS).
addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.addHeader(AUTH_HEADER_KEY
, BEARER_HEADER_VALUE
+ getToken())
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
}).build();
return new Retrofit.Builder()
.baseUrl(apiUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public static String getToken(){
final String token = "";
TokenEntity entity = UserUtil.getInstance().getToken();
long startTime = getStartTime();//获取token时的时间,单位:s
long currentTime = System.currentTimeMillis()/1000;//获取当前时间
//判断token的有效期是否在时间段内
if ((currentTime - startTime) < Long.parseLong(entity.getExpiresIn())){
token = entity.getToken();
}else {
//请求刷新token接口,获取新的token
}
return token;
}

然而当 token 失效时,出现了多个接口同时 token 失效,并都用 refreshToken 进行 token 刷新,出现接口调用错误,具体情况如下。

问题说明

  • 在 token 失效期间,多个接口同时请求刷新 token 这个接口,由于refreshToken 的有效性只为一次,当第一个请求接口去刷新 token,当接口还没有请求成功,token 还没有刷新,后面的接口请求时,token 也是失效的,需要刷新。后面的其他接口在使用原来的 refreshToken 刷新,则会失败,接口返回 4012 错误,退回到登录页面,与当初设想的效果不符。
    就像十字路口一样,如果南北和东西两条路同时通车,会发生事故的,我们必须要按照顺序进行。
    retrofit 刷新token并发处理

问题解决思路

在 retrofit 中有同步请求 execute() 和异步请求 enqueue(XXX) ,个人还没有找到可以实现多个接口并发,而且同步操作的方法。网上有人说建议使用 retrofit+Rxjava 可以实现,但是本人对 Rx 才上手还没有深入了解。所以能否解决这个问题,我还不知道。我解决问题的思路如下:
* 思路一
接口几乎是同时请求,那么我在请求每个接口的时候都增加一个 Thread.sleep(1000);请求 token 是异步操作,Android 不能在UI线程中进行同步请求,当网络比较慢的时候效果还可以,但是当网络状态良好,问题相当于没有解决。
* 思路二
使用 handler.sendMessage(); 使用消息队列进行处理,问题仍然没有解决
* 思路三
多线程同步,每次请求都是一个异步操作,想让获取 token 这个过程按照顺序来,就需要同步操作或者线程队列,并且获取 token 的方法使用同步操作。这个思路解决所遇到的问题。

解决办法

多线程同步可以解决当下的问题,同步有三种方法:
* 一是使用 synchronized 关键字,对方法进行同步。

public synchronized String getToken(){
final String token = "";
//对 token 进行有效判断,有效则使用原有的;无效则对 token 进行刷新操作
// ⚠️刷新 token 的接口要使用同步操作,否则无用
//....
return token;
}
  • 二是使用 synchronized(object){} 同步代码快的方法,把需要同步的操作放到 “{}” 内。与方法一是一样的
synchronized (this){
//需要同步的代码块
}
  • 三是使用重入锁 ReenreantLock 实现线程同步
private Lock mLock = new ReentrantLock();
public String getToken(){
mLock.lock();
try{
//需要上锁的代码块
}finally{
mLock.unlock();
}
}

此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

我使用的第一种解决办法,因为整个获取 token 的方法都需要同步实现, synchronized 关键字可以同步一个方法,也可以同步代码块(方法二),使用起来方便简洁,虽然没有 ReenreantLock 使用灵活,但是对于这个问题,使用 synchronized 已经足够了,代码如下:

public synchronized String getToken(){
final String token = "";
TokenEntity entity = UserUtil.getInstance().getToken();//获取登录时返回的 token 实体
long startTime = getStartTime();//获取 token 时的时间,单位:s
long currentTime = System.currentTimeMillis()/1000;//获取当前时间
//判断 token 的有效期是否在时间段内
if ((currentTime - startTime) < Long.parseLong(entity.getExpiresIn())) {
token = entity.getToken();
} else {
//根据 refreshToken 刷新 token,获得最新 token
//⚠️需要使用同步请求
}
return token];
}

涉及知识点

  • retroift 网络请求框架的使用
    官方网址:http://square.github.io/retrofit/ 讲述如何配置 retrofit,和接口的调用
    基础入门:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0915/3460.html 讲述 retrofit 初步使用、代码的实现、一些注意事项。
  • 多线程同步
    1、synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 既可以加在一段代码上,也可以加在方法上。
    2、ReentrantLock:官方说明是一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。
  • ⚠️多线程同步操作是一种耗费资源,耗时的操作,在开发中能不使用尽量不使用。

总结

在解决这个问题上,花费了我三天时间,想到延迟、消息队列、线程队列、同步请求等,经过所有的尝试后仍然解决不了问题,经过和同事的讨论,他们提供给我多线程同步的思路,经过查找资料,在获取 token 的方法上添加一个 synchronized 关键字,并使用 retrofit 的同步方法请求接口,问题得到解决。只有掌握更多的知识,才能够找到更好的思路。

参考文档
http://www.codeceo.com/article/java-multi-thread-sync.html 描述多线程同步的五种方法,并进行粗略的对比
https://*.com/questions/31021725/android-okhttp-refresh-expired-token 讲述retrofit 进行网络请求,token 过期之后发生的并发问题,并进行解决方法的讨论。
http://blog.csdn.net/jdsjlzx/article/details/52442113 使用RxJava+retrofit进行网络请求,解决 token 失效,并刷新 token 的方法。