Android网络实战篇——Token添加、过期判定以及处理(全局自动刷新)

Android日常开发中网络请求必不可少,一般来说每一个接口API都会设置Token,用来验证和做唯一识别,Token都会设置一个有效时间,那么Token失效了怎么办?怎样来更新Token?

首先怎样来判断Token失效呢?
  1. 与后端约定,保存到本地,约定时间到了就判定Token已过期
  2. 后端返回HTTP Code 401,客户端接到401后判定Token已过期
  3. 后端返回与客户端约定的自定义Code,客户端接到后判定Token已过期

以上三种判定方式,最“正规”的是第二种,第一种只在客户端做判断对于后端API的安全性存在很大问题;第三种自定义倒是还凑乎,但是不如第二种使用正规的HTTP Code,这对于客户端的统一网络拦截判断也有好处。(本篇采用第二种判定方式)

据笔者实际开发中有两种刷新方式:
  • 手动刷新
  • 自动刷新

手动刷新 就是当检测到Token失效后跳转到登录页,用户重新输入登录信息登录成功后接口返回新的Token,然后再使用新Token继续网络请求。

自动刷新 就是统一拦截网络请求Response,判断Code,然后检测到401后重新请求接口刷新Token,微信就是采用自动刷新Token方式(微信何曾让你重新登录过,除了微信号在其他设备登录)

因为每一个Token都会有一个有效时间,如果采取手动刷新的方式,若APP经常使用的话,每隔一段时间就需要重新输入登录信息,用户体验很不好,所以笔者在开发过程中都是自动刷新(当然需要后端接口的配合,有时候不配合也可以搞,下面说明)

实操

笔者通常使用OkHttp作为网络请求客户端,所以这里就使用OkHttp举例说明,其他也大同小异,无非是API不同而已,思想是相同的。

Token添加

通常Token被作为Header的一部分,添加到网络请求中,代码如下:

public static OkHttpClient createOkHttpClient(boolean isIntercept) {
        //网络日志
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                LogUtils.d(message);
            }
        });
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .readTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS)
                .connectTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS);
        if (isIntercept) {//拦截器
            builder.authenticator(new TokenAuthenticator())
                    .addInterceptor(new TokenInterceptor());
        }
        return builder.addInterceptor(interceptor)
                .build();
 }

public class TokenInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        request = request.newBuilder()
                    //登录后将Token保存到本地
                    .header(Config.HTTP_TOKEN_KET, Utils.getToken())
                    .build();
        return chain.proceed(request);
    }
}
Token过期判定——统一拦截HTTP Response(OkHttp 4.0.1

1.Interceptor拦截

OkHttp中提供了Interceptor网络拦截器,主要对Request、Response统一做一些参数修改、判断(包括增删改查),那么在这里就切开一个口子,获取HTTP Response Code对其进行判断,代码如下:

public class TokenInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        if (response.code()== HttpCode.REQUEST_TOKEN_INVALID) {//401
            //TODO Token失效,刷新Token
        }
        return response;
    }
}

2.Authenticator拦截

OkHttp中提供了一个Authenticator接口,其本身就是一个拦截器,但是与Interceptor不同的是Authenticator一般只对Response处理,源码中是这样说的:It
doesn't include the motivating request's HTTP headers or even its full URL; only the target server's hostname is sent to the proxy.Authenticator单纯用于身份/权限标识添加、验证,Authenticator也可以用于添加Token,这里用其判断Token过期,代码如下:

public class TokenAuthenticator implements Authenticator {
   
    @Nullable
    @Override
    public Request authenticate(@Nullable Route route, Response response) throws IOException {
        int code = response.code();
        if (code == HttpCode.REQUEST_TOKEN_INVALID) {
              //TODO Token过期
        }
        return response.request();
    }
}
//添加:okHttp.builder.authenticator(new TokenAuthenticator()).addInterceptor(new TokenInterceptor());                  
Token自动刷新

既然OkHttp专门提供了Authenticator用于身份核验,那么这里就使用Authenticator来自动刷新Token(但是Interceptor也是可以办到的),代码如下:

public class TokenAuthenticator implements Authenticator {
    /**
     * Token过期后调登录接口自动刷新
     * 若自动刷新失败,在Error同意处理并跳转到登录界面
     *
     * @param route
     * @param response
     * @return
     * @throws IOException
     */
    @Nullable
    @Override
    public Request authenticate(@Nullable Route route, Response response) throws IOException {
        int code = response.code();
        if (code == HttpCode.REQUEST_TOKEN_INVALID) {
            String account = Utils.getAccount();
            String encryptPassword = Utils.getEncryptPassword();
            if (Utils.isNonEmpty(account) && Utils.isNonEmpty(encryptPassword)) {
                HttpApi httpApi = RetrofitFactory.createRetrofit(false).create(HttpApi.class);//注意:刷新Token不能再拦截,否则就会陷入无限循环
              //同步刷新Token
                Call<SeengeneResponse<LoginResponseBody>> responseCall = httpApi.requestToken(new LoginRequestBody(account, encryptPassword));
                retrofit2.Response<SeengeneResponse<LoginResponseBody>> execute = responseCall.execute();
                if (!execute.isSuccessful()) {
                    return null;
                }
                SeengeneResponse<LoginResponseBody> body = execute.body();
                if (body != null) {
                    LoginResponseBody data = body.getData();
                    if (data != null) {
                        String token = data.getToken();
                        //保存Token
                        Utils.saveLoginToken(token);
                        return response.request().newBuilder()
                                .header(Config.HTTP_TOKEN_KET, token)
                                .build();
                    }
                }
            }
        }
        return response.request();
    }
}

以上代码需要注意一下几点:

  1. 刷新Token接口不能再拦截判断Token是否过期,因为若服务器出现问题老是返回401那客户端就不断刷新了(HTTP FAILED: java.net.ProtocolException: Too many follow-up requests: 21重定向大于21阈值就会抛出此异常),这里只需要自动刷新一次,若没有刷新成功就转到登录界面
    具体代码设置如下(创建RetrofitFactory.createRetrofit(false))
public static OkHttpClient  createOkHttpClient(boolean isIntercept) {
        //网络日志
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                LogUtils.d(message);
            }
        });
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .readTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS)
                .connectTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(Config.HTTP_TIMEOUT, TimeUnit.SECONDS);
        if (isIntercept) {//不再拦截网络请求
            builder.authenticator(new TokenAuthenticator())
                    .addInterceptor(new TokenInterceptor());
        }
        return builder.addInterceptor(interceptor)
                .build();
    }
  1. 这里Token刷新接口和登录接口是同一个(若没有RefreshToken API的话可以将就代替),具体操作就是第一次登录之后将登录信息保存到本地(这里需要注意的是出于安全性考虑不能将原始密码直接保存到本地必须是经过不可逆装换后才能保存),Token过期需要刷新的时候自动填充到登录接口中进行网络请求。
  1. 若刷新过程中出现异常,需要集中捕获然后跳转到登录界面,一般不要无限刷新,而且异常捕获要统一处理,事例代码:
/**
 * 观察者基类
 *
 * @param <T>
 */
public abstract class BaseSubscriber<T> extends ResourceSubscriber<BaseResponse<T>> {
    protected Context mContext;
    protected BaseView mBaseView;
    /**
     * 表示哪一个网络请求,例如一个界面有不同的网络请求,同一个方法可以通过type来区分
     */
    @Nullable
    protected Object mType;

    public BaseSubscriber(Context context, BaseView view, @Nullable Object type) {
        mContext = context;
        mBaseView = view;
        mType = type;
    }

    public BaseSubscriber(Context context, BaseView view) {
        this(context, view, null);
    }

    /**
     * 错误统一回调
     */
    @CallSuper
    @Override
    public void onError(Throwable throwable) {
        mBaseView.showComplete(mType);//onError与onComplete只调用其一,所以需要手动调用mBaseView.showComplete(mType)结束loading
        if (throwable instanceof SocketTimeoutException) {//网络超时
            onFail(HttpCode.REQUEST_TIMEOUT, mContext.getString(R.string.request_state_timeout));
        } else if (throwable instanceof ApiException) {//后台API异常
            LogUtils.d("ApiException");
            ApiException apiException = (ApiException) throwable;
            onFail(apiException.getCode(), apiException.getMsg());
        } else if (throwable instanceof HttpException) {//在这里统一处理
            HttpException httpException = (HttpException) throwable;
            if (httpException.code() == HttpCode.REQUEST_TOKEN_INVALID) {//token过期以及刷新失败处理
                mBaseView.showError(R.string.token_invalid_prompt);
                Utils.clearLoginInfo();
                App.getApp().finishAllActivity();
                Bundle bundle = new Bundle();
                bundle.putInt(IntentKey.LOGIN_ACTIVITY, Type.BasicFun.LOGIN);
                IntentUtil.startActivity(mContext, LoginActivity.class, bundle);
            } else {
                onFail(HttpCode.REQUEST_NET_ERROR, mContext.getString(R.string.request_state_netowrk_error));
            }
        } else if (throwable instanceof JsonSyntaxException) {//Json解析错误
            onFail(HttpCode.REQUEST_ERROR, mContext.getString(R.string.request_state_fail));
        } else {//TODO 若有其他异常再加
            onFail(HttpCode.REQUEST_ERROR, mContext.getString(R.string.request_state_fail));
        }
    }

}

说明:这里使用的是RxJava+Retrofit搭配进行网络请求,所以定义BaseSubscriber基类,将所有的异常捕获统一处理

综上,Token过期处理大致思想就是上述,具体实现不同网络客户端实现方式不同,OkHttp中Authenticator和Interceptor都可用来Token添加、Token失效判定以及处理,读者有不明白之处或有其他观点请留言交流!

对于多线程情况下Token刷新解决方案请转到《Android网络实战篇——单进程多线程情况下Token自动刷新方案探讨》

觉得不错就给个赞吧,谢谢!!!

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

推荐阅读更多精彩内容