android 动态刷新token

项目中验证用户会采用实时变更token的方式,一个refreshToken 五分钟过期一次,一个accessToken 一周失效。refreshToken过期需要刷新替换,accessToken需要过期退出登录。开发中会遇到场景:当前refreshToken已经过期,存在异步几个接口同时持有过期的refreshToken请求。这样的结果可想而知!

项目使用的是retrofit,结合retrofit 的动态代理设计模式,可以引以为用:

  • 1.动态代理替换service抽象接口
    /**
     * 接口service
     *
     * @param service 具体业务service
     * @param <T>     泛型
     * @return service 实例
     */
    public static <T> T getService(Class<T> service) {
        return createProxy(service);
    }

    private static <S> S createProxy(Class<S> serviceClass) {
        S s = getInstance().retrofit.create(serviceClass);
        return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass}, new ProxyHandler(s));
    }

下面是ProxyHandler 类,返回请求对象观察者:

/**
 * @author 桂雁彬
 * @date 2019-12-18.
 * GitHub:
 * description:
 */
public class ProxyHandler implements java.lang.reflect.InvocationHandler {
    private final Object target;
    private boolean mIsTokenNeedRefresh = true;
    private final static String TOKEN = "token";

    ProxyHandler(@NonNull final Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        Object r = method.invoke(target, args);
        if (!(r instanceof Observable)) {
            return r;
        }
        return Observable.just(true)
                .flatMap((Function<Object, ObservableSource<?>>) o -> (Observable<?>) method.invoke(target, args)).retryWhen(new RetryWithDelay(2, 1000));
    }
}

由invoke 的返回可看具体操作类RetryWithDelay

public class RetryWithDelay implements Function<Observable<? extends Throwable>, Observable<?>> {
    private final int maxRetries;
    private final int retryDelayMillis;
    private long currentRefreshTime;
    private static final long REFRESH_TOKEN_TIME = 200;
    private boolean oneTimeRefresh;
    public RetryWithDelay(int maxRetries, int retryDelayMillis) {
        this.maxRetries = maxRetries;
        this.retryDelayMillis = retryDelayMillis;
    }
    @Override
    public Observable<?> apply(Observable<? extends Throwable> observable) {
        return observable.flatMap(new Function<Throwable, ObservableSource<?>>() {
            @Override
            public ObservableSource<?> apply(Throwable throwable) throws Exception {
                LogUtils.d("TtSy", "throwable:" + throwable);
                LogUtils.d("==refreshTpoken", "response:" + "onUnAuth" + "  thread:" + Thread.currentThread());

                if (throwable instanceof ApiException) {
                    ApiException apiException = (ApiException) throwable;
                    if (apiException.getCode() == 20001) { //token 过期 直接退出登录
                        // apiCode = 16  游客token过期
                        if (!oneTimeRefresh){
                            EventBus.getDefault().post(new BaseLoginOutEvent());
                            oneTimeRefresh=true;
                        }
                    }else  if (apiException.getCode() == 20002) { //持续刷新token

                        return onUnAuth(throwable);
                    }
                } else if (throwable instanceof HttpException) {
                    HttpException httpException = (HttpException) throwable;
                    if (httpException.code() == 401) {
                        if (!oneTimeRefresh){
                            EventBus.getDefault().post(new BaseLoginOutEvent());
                            oneTimeRefresh=true;
                        }
                    }
                }
                return Observable.error(throwable);
            }
        });
    }
    private Observable<?> onUnAuth(Throwable throwable) {
        synchronized (ProxyHandler.class) {
            // 防止多次调用
            if (System.currentTimeMillis() - currentRefreshTime < REFRESH_TOKEN_TIME) {
                return Observable.just(true);
            }
            return Observable.intervalRange(1, maxRetries, 0, retryDelayMillis, TimeUnit.MILLISECONDS)
                    .flatMap(new Function<Long, ObservableSource<?>>() {
                        @Override
                        public ObservableSource<?> apply(Long aLong) throws Exception {
                            LogUtils.d("==refreshTpoken", "response:" + "onUnAuth" + "  thread:" + Thread.currentThread());

                            if (aLong == maxRetries) {
                                return Observable.error(new ApiException("未知错误", -1));
                            }
                            return refreshToken();
                        }
                    });
        }
    }
    private Observable<?> refreshToken() {
        synchronized (ProxyHandler.class) {
            Map<String, Object> map = new HashMap<>(2);
            return RetrofitHelp.getService(RefreshTokenApiService.class)
                    .refreshToken(map)
                    .flatMap((Function<ResultEntity<RefreshToken>, ObservableSource<?>>) refreshTokenResultEntity -> {
                        LogUtils.d("==refreshTpoken", "response:" + refreshTokenResultEntity + "  thread:" + Thread.currentThread());
                        RefreshToken data = refreshTokenResultEntity.getData();
                        if (data != null && !TextUtils.isEmpty(data.getToken())) {
                            putToken(data.getToken());
                            currentRefreshTime = System.currentTimeMillis();
                            return Observable.just(true);
                        }
                        return Observable.error(new ApiException("未知错误", -1));
                    });
        }
    }

    /**
     * 保存本地token
     *
     * @param token
     */
    public void putToken(String token) {
        PSP.getInstance().remove(FinalKey.USER_TOKEN);
        PSP.getInstance().put(FinalKey.USER_TOKEN, token);
    }

}

上面的代码主要的思路是对应服务协议token失效会返回对应的code,当前 code 20002时去同步刷新token并返回最终的Observable,这样其他请求并不会同时异步持有过期token,会在本次刷新完后持有最新token刷新。

注:上面的

   if (throwable instanceof ApiException) {
                    ApiException apiException = (ApiException) throwable;
                    if (apiException.getCode() == 20001) { //token 过期 直接退出登录
                        // apiCode = 16  游客token过期
                        if (!oneTimeRefresh){
                            EventBus.getDefault().post(new BaseLoginOutEvent());
                            oneTimeRefresh=true;
                        }
                    }else  if (apiException.getCode() == 20002) { //持续刷新token

                        return onUnAuth(throwable);
                    }

是在网络请求根据服务端返回对应的code,如果不是请求成功码会抛出异常,由rxJava 处理最终拿到回调throwable。
可以在自定义Converter 中实现:

public class CustomGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final TypeAdapter<T> adapter;

    CustomGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public T convert(@NonNull ResponseBody value) throws IOException {
        String string = value.string();

        LogUtils.d("==temp==",string);
        ResultEntity baseNewEntity = gson.fromJson(string, ResultEntity.class);

        if (baseNewEntity == null) {
            value.close();
            throw new ApiException("数据错误", -1);
        }

        if (!baseNewEntity.isSuccess()) {
            value.close();
            throw new ApiException(baseNewEntity.getMsg(), baseNewEntity.getCode());
        }

        MediaType contentType = value.contentType();
        Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
        InputStream inputStream = new ByteArrayInputStream(string.getBytes());
        Reader reader = new InputStreamReader(inputStream, charset);
        JsonReader jsonReader = gson.newJsonReader(reader);
        try {
            return adapter.read(jsonReader);
        } finally {
            value.close();
        }
    }

    private static final Charset UTF_8 = Charset.forName("UTF-8");
}

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

推荐阅读更多精彩内容