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失效呢茎刚?
- 與后端約定襟锐,保存到本地,約定時(shí)間到了就判定Token已過(guò)期
- 后端返回HTTP Code 401膛锭,客戶端接到401后判定Token已過(guò)期
- 后端返回與客戶端約定的自定義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):
- 刷新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();
}
- 這里Token刷新接口和登錄接口是同一個(gè)(若沒(méi)有RefreshToken API的話可以將就代替)迄委,具體操作就是第一次登錄之后將登錄信息保存到本地(這里需要注意的是出于安全性考慮不能將原始密碼直接保存到本地必須是經(jīng)過(guò)不可逆裝換后才能保存)褐筛,Token過(guò)期需要刷新的時(shí)候自動(dòng)填充到登錄接口中進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
- 若刷新過(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)刷新方案探討》