retrofit 刷新token并发处理

背景说明

在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 的方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容