自动刷新token方案

4,712 阅读4分钟

本文结合Rxjava Okhttp Retrofit 开源方案予以实现

制定目标

  • 执行业务请求时,accessToken 失效,自动执行refreshToken携带最新accessToken重试之前的业务请求
  • 多业务请求并发访问时,所有请求均失效,保证仅有一次refreshToken操作
  • refreshToken进行合理的节流
  • 业务请求+refreshToken 合理的降级策略
  • 特殊场景:NoRefreshToken 白名单策略

落地实现

refreshToken时序图

  • 执行业务请求时,accessToken 失效,自动执行refreshToken携带最新accessToken重试之前的业务请求

    1. 设计模式之全局动态代理模式
    2. Rxjava retryWhen 操作符
    Class<TestApi> aClass = TestApi.class;
    testApi = retrofit.create(aClass);
    TokenRefreshProxy proxy = new TokenRefreshProxy(testApi);
    testApi = (TestApi) Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aClass}, proxy);
    

    动态代理以保证每一个接口执行时都会回调TokenRefreshProxy#invoke ,在这个方法中控制刷新token刷新时机

    @Override
      public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
          return Observable.defer(new Callable<ObservableSource<TokenWrapper>>() {
              @Override
              public ObservableSource<TokenWrapper> call() throws Exception {
                  // 请求接口时携带的参数中的token 和全局token若不一致 就进行替换
                  String declareToken = getDeclareToken(method, args); // 获取请求参数中的token
                  String globalToken = MyApp.sToken;
                  if (!TextUtils.isEmpty(globalToken) && !TextUtils.equals(declareToken, globalToken)) {
                      return Observable.just(new TokenWrapper(true, globalToken));
                  } else {
                      return Observable.just(new TokenWrapper(false, null));
                  }
              }
          }).flatMap(new Function<TokenWrapper, ObservableSource<?>>() {
              @Override
              public ObservableSource<?> apply(TokenWrapper wrapper) throws Exception {
                  if (wrapper.isNeedRefresh) {
                      // 替换请求参数为最新的token
                      updateMethodToken(method, args, wrapper.newestToken);
                  }
                  // 执行业务请求
                  ObservableSource<?> observableSource = (ObservableSource<?>) method.invoke(proxyObj, args);
                  return observableSource;
              }
          }).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
              @Override
              public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {
                  return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                      @Override
                      public ObservableSource<?> apply(Throwable throwable) throws Exception {
                          if(throwable instanceof TokenInvalidException){
                              // 刷新token
                              return RefreshTokenLoader.getInstance().getTokenLocked();
                          }
                          // 其他异常直接向上抛出
                          return Observable.error(throwable);
                  });
              }
          });
      }
    
  • 多业务请求并发访问时,所有请求均失效,保证仅有一次refreshToken操作

    1. 多线程并发访问同步控制
    2. PublishSubject 操作符, 四种Subject介绍
    private class RefreshTokenLoader{
       // 当前refreshToken操作状态
       // 原子性 Atomic**, 不可分割操作 
       // volatile 保证线程可见性
       private volatile AtomicBoolean refreshTokenFlag = new AtmoicBoolean(false)
       private PublishSubject<?> publishSubject;
       // 单例
       private RefreshTokenLoader() {
    
       }
       public static RefreshTokenLoader getInstance() {
          return Holder.sInstance;
       }
       public static class Holder {
          private static final RefreshTokenLoader sInstance = new RefreshTokenLoader();
       }
       
       public synchronize Observable<?> getTokenLocked() {
          if (refreshTokenFlag.compareAndSet(false, true)) {
              // 传统做法是对boolean 做判断 + 复制 ,本身就是一个非原子操作,所以这里选择AtomicBoolean API
              // 每次token失效时,都需要重新实例化一个publishSubject
              publishSubject = PublishSubject.create();
              Observable<?> refreshTokenObservable = createRefreshTokenObservable.doOnNext(tokenEntity -> {
                // .... 缓存最新的accessToken
                refreshTokenFlag.set(false);
              }).doOnError(throwable -> {
                // ... 跳转到登陆页面
                refreshTokenFlag.set(false)
              }).subscribeOn(Schedulers.io());
              refreshTokenObservable.subscribe(publishSubject);
          } else {
              // get token locked
          }
          return publishSubject;
      }
    }
    
  • refreshToken进行合理的节流

    某种场景:A,B 请求,A请求耗时10s, B请求耗时5s, refreshToken 耗时2s, A,B请求同时并发,B先感知到token失效,并完成换取token工作,此时A请求才感知到token失效,假设token有效期为 > 3s ,那么A请求就没有必要再去换取token了,直接复用B请求换取的token进行重试

    RefreshTokenLoader.java
    public Observable<?> getTokenLocked(){
        //.....  
        Observable<?> refreshTokenObservable = createRefreshTokenObservable.doOnNext(tokenEntity -> {
            // .... 缓存最新的accessToken
            // saveRefreshTokenTime 此处记录当前时间为t1
            // 若此处server返回bisCode 异常,需要手动调用:
            publishSubject.onError() ; // 防止队列中剩余请求做不必要的额外重试
            sp.edit().putLong("t1", System.currentTimeMillis());
          })
        // ....  
    }
    TokenRefreshProxy.java
    xxx.retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
          @Override
          public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {
              return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                  @Override
                  public ObservableSource<?> apply(Throwable throwable) throws Exception {
                      if(throwable instanceof TokenInvalidException){
                        long currentTime = System.currentTimeMillis();
                        long t1 = sp.getLong("t1");
                        // 规定30s之内token均有效 ,直接复用上次获取的token即可 
                        if (t1 != 0l && currentTime - t1 <= 30 * 1000 && !TextUtils.isEmpty(globalToken)) {
                          return Observable.just(globalToken);
                        }
                          // 刷新token
                        return RefreshTokenLoader.getInstance().getTokenLocked();
                      }
                      // 其他异常直接向上抛出
                      return Observable.error(throwable);
              });
          }
      });
    
    

    这里主要目的是多请求并发访问情况下为了减少不必要额外refreshToken操作。当然你也可以将这个30s 设置为token有效期。

  • 业务请求+refreshToken 合理的降级策略

    1. zipWith concatMap 操作符

    每一个业务请求重试次数不能超过指定次数

    TokenRefreshProxy.java
    int maxRetryCount = 3;
    XXX.retryWhen(throwableObservable -> {
       return throwableObservable.zipWith(Observable.range(1, maxRetryCount), (BiFunction<Throwable, Integer, ThrowableWrapper>) (throwable, integer) -> {
              if (throwableObservable instanceof TokenInvalidException) {
                  if (topActivityIsLogin()) {
                      return new ThrowableWrapper(throwable, maxRetryCount);
                  }
                  // token失效正常重试
                  return new ThrowableWrapper(throwable, integer);
              }
              return new ThrowableWrapper(throwable, integer);
          }).concatMap(throwableWrapper -> {
              final int retryCount = throwableWrapper.getRetryCount();
              if (retryCount >= maxRetryCount) {
                  // 重试次数用完了 || 当前已经跳转到登陆页了 || 其他非token过期异常 直接向上抛
                  return Observable.error(throwableWrapper.getSourceThrowable());
              }
              long currentTime = System.currentTimeMillis();
              long t1 = sp.getLong("t1");
              // 规定30s之内token均有效 ,直接复用上次获取的token即可
              if (t1 != 0l && currentTime - t1 <= 30 * 1000 && !TextUtils.isEmpty(globalToken)) {
                  return Observable.just(globalToken);
              }
              // 刷新token
              return RefreshTokenLoader.getInstance().getTokenLocked();
          })
    })
    
  • 特殊场景:NoRefreshToken 白名单策略

    特定场景下,某些接口不需要这套token验证机制

    1. 运行时注解定义及其获取
    @Documented
    @Target(METHOD)
    @Retention(RUNTIME)
    public @interface NoTokenProxy {
    
    }
    TokenRefreshProxy.java
    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        Annotation[] declaredAnnotations = method.getDeclaredAnnotations();
        for (int i = 0; i < declaredAnnotations.length; i++) {
              Annotation annotation = declaredAnnotations[i];
              if (annotation instanceof NoTokenProxy) {
                  // 说明不需要加token 重试代理 直接执行实际函数并返回
                  return method.invoke(proxyObj, args);
              }
        }
        ///... 刷新token机制代码  
    }