Android日常开发中网络请求必不可少,一般来说每一个接口API都会设置Token,用来验证和做唯一识别,Token都会设置一个有效时间,那么Token失效了怎么办?怎样来更新Token?
首先怎样来判断Token失效呢?
- 与后端约定,保存到本地,约定时间到了就判定Token已过期
- 后端返回HTTP Code 401,客户端接到401后判定Token已过期
- 后端返回与客户端约定的自定义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();
}
}
以上代码需要注意一下几点:
- 刷新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();
}
- 这里Token刷新接口和登录接口是同一个(若没有RefreshToken API的话可以将就代替),具体操作就是第一次登录之后将登录信息保存到本地(这里需要注意的是出于安全性考虑不能将原始密码直接保存到本地必须是经过不可逆装换后才能保存),Token过期需要刷新的时候自动填充到登录接口中进行网络请求。
- 若刷新过程中出现异常,需要集中捕获然后跳转到登录界面,一般不要无限刷新,而且异常捕获要统一处理,事例代码:
/**
* 观察者基类
*
* @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自动刷新方案探讨》