Android網(wǎng)絡(luò)實(shí)戰(zhàn)篇——Token添加、過(guò)期判定以及處理(全局自動(dòng)刷新)

Android日常開(kāi)發(fā)中網(wǎng)絡(luò)請(qǐng)求必不可少虚循,一般來(lái)說(shuō)每一個(gè)接口API都會(huì)設(shè)置Token同欠,用來(lái)驗(yàn)證和做唯一識(shí)別,Token都會(huì)設(shè)置一個(gè)有效時(shí)間横缔,那么Token失效了怎么辦铺遂?怎樣來(lái)更新Token?

首先怎樣來(lái)判斷Token失效呢茎刚?
  1. 與后端約定襟锐,保存到本地,約定時(shí)間到了就判定Token已過(guò)期
  2. 后端返回HTTP Code 401膛锭,客戶端接到401后判定Token已過(guò)期
  3. 后端返回與客戶端約定的自定義Code粮坞,客戶端接到后判定Token已過(guò)期

以上三種判定方式,最“正規(guī)”的是第二種初狰,第一種只在客戶端做判斷對(duì)于后端API的安全性存在很大問(wèn)題莫杈;第三種自定義倒是還湊乎,但是不如第二種使用正規(guī)的HTTP Code跷究,這對(duì)于客戶端的統(tǒng)一網(wǎng)絡(luò)攔截判斷也有好處姓迅。(本篇采用第二種判定方式)

據(jù)筆者實(shí)際開(kāi)發(fā)中有兩種刷新方式:
  • 手動(dòng)刷新
  • 自動(dòng)刷新

手動(dòng)刷新 就是當(dāng)檢測(cè)到Token失效后跳轉(zhuǎn)到登錄頁(yè)敲霍,用戶重新輸入登錄信息登錄成功后接口返回新的Token俊马,然后再使用新Token繼續(xù)網(wǎng)絡(luò)請(qǐng)求丁存。

自動(dòng)刷新 就是統(tǒng)一攔截網(wǎng)絡(luò)請(qǐng)求Response,判斷Code柴我,然后檢測(cè)到401后重新請(qǐng)求接口刷新Token解寝,微信就是采用自動(dòng)刷新Token方式(微信何曾讓你重新登錄過(guò),除了微信號(hào)在其他設(shè)備登錄)

因?yàn)槊恳粋€(gè)Token都會(huì)有一個(gè)有效時(shí)間艘儒,如果采取手動(dòng)刷新的方式聋伦,若APP經(jīng)常使用的話,每隔一段時(shí)間就需要重新輸入登錄信息界睁,用戶體驗(yàn)很不好觉增,所以筆者在開(kāi)發(fā)過(guò)程中都是自動(dòng)刷新(當(dāng)然需要后端接口的配合,有時(shí)候不配合也可以搞翻斟,下面說(shuō)明)

實(shí)操

筆者通常使用OkHttp作為網(wǎng)絡(luò)請(qǐng)求客戶端逾礁,所以這里就使用OkHttp舉例說(shuō)明,其他也大同小異访惜,無(wú)非是API不同而已嘹履,思想是相同的。

Token添加

通常Token被作為Header的一部分债热,添加到網(wǎng)絡(luò)請(qǐng)求中砾嫉,代碼如下:

public static OkHttpClient createOkHttpClient(boolean isIntercept) {
        //網(wǎng)絡(luò)日志
        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過(guò)期判定——統(tǒng)一攔截HTTP Response(OkHttp 4.0.1

1.Interceptor攔截

OkHttp中提供了Interceptor網(wǎng)絡(luò)攔截器,主要對(duì)Request窒篱、Response統(tǒng)一做一些參數(shù)修改焕刮、判斷(包括增刪改查),那么在這里就切開(kāi)一個(gè)口子墙杯,獲取HTTP Response Code對(duì)其進(jìn)行判斷济锄,代碼如下:

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中提供了一個(gè)Authenticator接口霍转,其本身就是一個(gè)攔截器荐绝,但是與Interceptor不同的是Authenticator一般只對(duì)Response處理,源碼中是這樣說(shuō)的: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單純用于身份/權(quán)限標(biāo)識(shí)添加避消、驗(yàn)證低滩,Authenticator也可以用于添加Token,這里用其判斷Token過(guò)期,代碼如下:

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過(guò)期
        }
        return response.request();
    }
}
//添加:okHttp.builder.authenticator(new TokenAuthenticator()).addInterceptor(new TokenInterceptor());                  
Token自動(dòng)刷新

既然OkHttp專(zhuān)門(mén)提供了Authenticator用于身份核驗(yàn)岩喷,那么這里就使用Authenticator來(lái)自動(dòng)刷新Token(但是Interceptor也是可以辦到的)恕沫,代碼如下:

public class TokenAuthenticator implements Authenticator {
    /**
     * Token過(guò)期后調(diào)登錄接口自動(dòng)刷新
     * 若自動(dòng)刷新失敗,在Error同意處理并跳轉(zhuǎn)到登錄界面
     *
     * @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不能再攔截纱意,否則就會(huì)陷入無(wú)限循環(huán)
              //同步刷新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();
    }
}

以上代碼需要注意一下幾點(diǎn):

  1. 刷新Token接口不能再攔截判斷Token是否過(guò)期婶溯,因?yàn)槿舴?wù)器出現(xiàn)問(wèn)題老是返回401那客戶端就不斷刷新了(HTTP FAILED: java.net.ProtocolException: Too many follow-up requests: 21重定向大于21閾值就會(huì)拋出此異常),這里只需要自動(dòng)刷新一次,若沒(méi)有刷新成功就轉(zhuǎn)到登錄界面
    具體代碼設(shè)置如下(創(chuàng)建RetrofitFactory.createRetrofit(false))
public static OkHttpClient  createOkHttpClient(boolean isIntercept) {
        //網(wǎng)絡(luò)日志
        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) {//不再攔截網(wǎng)絡(luò)請(qǐng)求
            builder.authenticator(new TokenAuthenticator())
                    .addInterceptor(new TokenInterceptor());
        }
        return builder.addInterceptor(interceptor)
                .build();
    }
  1. 這里Token刷新接口和登錄接口是同一個(gè)(若沒(méi)有RefreshToken API的話可以將就代替)迄委,具體操作就是第一次登錄之后將登錄信息保存到本地(這里需要注意的是出于安全性考慮不能將原始密碼直接保存到本地必須是經(jīng)過(guò)不可逆裝換后才能保存)褐筛,Token過(guò)期需要刷新的時(shí)候自動(dòng)填充到登錄接口中進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
  1. 若刷新過(guò)程中出現(xiàn)異常叙身,需要集中捕獲然后跳轉(zhuǎn)到登錄界面渔扎,一般不要無(wú)限刷新,而且異常捕獲要統(tǒng)一處理信轿,事例代碼:
/**
 * 觀察者基類(lèi)
 *
 * @param <T>
 */
public abstract class BaseSubscriber<T> extends ResourceSubscriber<BaseResponse<T>> {
    protected Context mContext;
    protected BaseView mBaseView;
    /**
     * 表示哪一個(gè)網(wǎng)絡(luò)請(qǐng)求晃痴,例如一個(gè)界面有不同的網(wǎng)絡(luò)請(qǐng)求,同一個(gè)方法可以通過(guò)type來(lái)區(qū)分
     */
    @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);
    }

    /**
     * 錯(cuò)誤統(tǒng)一回調(diào)
     */
    @CallSuper
    @Override
    public void onError(Throwable throwable) {
        mBaseView.showComplete(mType);//onError與onComplete只調(diào)用其一,所以需要手動(dòng)調(diào)用mBaseView.showComplete(mType)結(jié)束loading
        if (throwable instanceof SocketTimeoutException) {//網(wǎng)絡(luò)超時(shí)
            onFail(HttpCode.REQUEST_TIMEOUT, mContext.getString(R.string.request_state_timeout));
        } else if (throwable instanceof ApiException) {//后臺(tái)API異常
            LogUtils.d("ApiException");
            ApiException apiException = (ApiException) throwable;
            onFail(apiException.getCode(), apiException.getMsg());
        } else if (throwable instanceof HttpException) {//在這里統(tǒng)一處理
            HttpException httpException = (HttpException) throwable;
            if (httpException.code() == HttpCode.REQUEST_TOKEN_INVALID) {//token過(guò)期以及刷新失敗處理
                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解析錯(cuò)誤
            onFail(HttpCode.REQUEST_ERROR, mContext.getString(R.string.request_state_fail));
        } else {//TODO 若有其他異常再加
            onFail(HttpCode.REQUEST_ERROR, mContext.getString(R.string.request_state_fail));
        }
    }

}

說(shuō)明:這里使用的是RxJava+Retrofit搭配進(jìn)行網(wǎng)絡(luò)請(qǐng)求财忽,所以定義BaseSubscriber基類(lèi)倘核,將所有的異常捕獲統(tǒng)一處理

綜上,Token過(guò)期處理大致思想就是上述即彪,具體實(shí)現(xiàn)不同網(wǎng)絡(luò)客戶端實(shí)現(xiàn)方式不同笤虫,OkHttp中Authenticator和Interceptor都可用來(lái)Token添加、Token失效判定以及處理祖凫,讀者有不明白之處或有其他觀點(diǎn)請(qǐng)留言交流琼蚯!

對(duì)于多線程情況下Token刷新解決方案請(qǐng)轉(zhuǎn)到《Android網(wǎng)絡(luò)實(shí)戰(zhàn)篇——單進(jìn)程多線程情況下Token自動(dòng)刷新方案探討》

覺(jué)得不錯(cuò)就給個(gè)贊吧,謝謝;菘觥T馐!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稠屠,一起剝皮案震驚了整個(gè)濱河市峦睡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌权埠,老刑警劉巖榨了,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異攘蔽,居然都是意外死亡龙屉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)满俗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)转捕,“玉大人,你說(shuō)我怎么就攤上這事唆垃∥逯ィ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵辕万,是天一觀的道長(zhǎng)枢步。 經(jīng)常有香客問(wèn)我沉删,道長(zhǎng),這世上最難降的妖魔是什么醉途? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任矾瑰,我火速辦了婚禮,結(jié)果婚禮上结蟋,老公的妹妹穿的比我還像新娘脯倚。我一直安慰自己渔彰,他們只是感情好嵌屎,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著恍涂,像睡著了一般宝惰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上再沧,一...
    開(kāi)封第一講書(shū)人閱讀 52,736評(píng)論 1 312
  • 那天尼夺,我揣著相機(jī)與錄音,去河邊找鬼炒瘸。 笑死淤堵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的顷扩。 我是一名探鬼主播拐邪,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼隘截!你這毒婦竟也來(lái)了扎阶?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤婶芭,失蹤者是張志新(化名)和其女友劉穎东臀,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體犀农,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惰赋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呵哨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谤逼。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖仇穗,靈堂內(nèi)的尸體忽然破棺而出流部,到底是詐尸還是另有隱情,我是刑警寧澤纹坐,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布枝冀,位于F島的核電站舞丛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏果漾。R本人自食惡果不足惜球切,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绒障。 院中可真熱鬧吨凑,春花似錦、人聲如沸户辱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)庐镐。三九已至恩商,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間必逆,已是汗流浹背怠堪。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留名眉,地道東北人粟矿。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像损拢,于是被迫代替她去往敵國(guó)和親陌粹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容