本文结合
Rxjava
Okhttp
Retrofit
开源方案予以实现
制定目标
- 执行业务请求时,
accessToken
失效,自动执行refreshToken
,携带最新accessToken
重试之前的业务请求 - 多业务请求并发访问时,所有请求均失效,保证仅有一次
refreshToken
操作 - 对
refreshToken
进行合理的节流 - 业务请求+
refreshToken
合理的降级策略 - 特殊场景:
NoRefreshToken
白名单策略
落地实现
-
执行业务请求时,
accessToken
失效,自动执行refreshToken
,携带最新accessToken
重试之前的业务请求- 设计模式之全局动态代理模式
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
操作- 多线程并发访问同步控制
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
合理的降级策略每一个业务请求重试次数不能超过指定次数
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验证机制
- 运行时注解定义及其获取
@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机制代码 } 复制代码