retrofit 刷新token并发处理

来源:互联网 发布:网络汉语教师 编辑:程序博客网 时间:2024/05/19 16:36

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

背景说明

在app开发中,我们需要保证用户登录之后,如果没有在其他设备上登录,则不需要再次登录,很多都会使用 token 作为安全令牌,开始阶段都会在登录时候获取,一直使用到下次登录。这样的 token 没有什么安全性可言,所以大多app都会做 token 时效性处理。
刷新 token 流程图如下:
刷新 token.png
根据刷新 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 错误,退回到登录页面,与当初设想的效果不符。
    就像十字路口一样,如果南北和东西两条路同时通车,会发生事故的,我们必须要按照顺序进行。
    并发.png.jpeg

问题解决思路

在 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://stackoverflow.com/questions/31021725/android-okhttp-refresh-expired-token 讲述retrofit 进行网络请求,token 过期之后发生的并发问题,并进行解决方法的讨论。
http://blog.csdn.net/jdsjlzx/article/details/52442113 使用RxJava+retrofit进行网络请求,解决 token 失效,并刷新 token 的方法。

原创粉丝点击