科学的解决Http Token拦截器TokenInterceptor实现
1.写在前面
在做项目的时候,有时对接口要求比较严谨。先介绍下情况。
- 我这边Http 方式采用的是 OKhttp+Retrofit
- 后台一共分为三种token,分别是实名token(accessToken),匿名token(oauthToken),刷新token(refreshToken),不同的token用途不一样,有的作为请求参数放在请求体中,有的作为头部。
2.解决
如果你和我情况差不多,那恭喜你,可以通过这篇博客解决你的一些问题;如果不是,也可以瞧瞧,可能也能得到一些启发。
先把oauthToken拿出来单独说,因为它比较简单,只是单独注册或者忘记密码使用,一般来说后台会给个单独的get请求接口用于获取oauthToen,然后我们对它进行sp缓存,并且一般来说作为它会作为请求参数放在请求体中作为注册或者忘记密码的参数,注意这里是参数而不是头部,特别要注意,一般后台都会这样设计,所以,此token比较简单。
一般来说accessToken是会作为头部放在Http请求中,比如我们的Header规则是:
key—>Authorization
value–>token_type access_token
相信大家都看得懂,不一样的是,我们的值不仅仅是个accessToken,而是一个其他变量和它拼接而成的,当然这个影响不大,因为这些值都会在我们登录成功后返回给我们,我们只需要在登录成功后用SP将他们缓存即可,所以这里我的头部配置代码如下:
//添加头部token .addHeader("Authorization",getDefaultTokenType()+" "+SPUtils.getSharedStringData(BaseApplication.getAppContext(),AppConstant.ACCESS_TOKEN_KEY)) /** * 从Sp里面取TokenType,如果没有则返回默认确定的值 * @return */ public String getDefaultTokenType(){
String tokenType = SPUtils.getSharedStringData(BaseApplication.getAppContext(), AppConstant.TOKEN_TYPE_KEY); if(TextUtils.isEmpty(tokenType)){
tokenType = "token";
//最好保存在SP中
SPUtils.setSharedStringData(BaseApplication.getAppContext(), AppConstant.TOKEN_TYPE_KEY, tokenType);
return tokenType;
}
return tokenType;
}
这应该算第一步,根据后台定义的规则,配置好头部。有些人担心,有些接口请求并不需要头部,我们配置了有没有影响,比如登录接口,我们都没有获取到accessToken,怎么会请求需要头部呢。这里不用担心,没事的。
我们知道所有的HTTP响应行中有自己的响应码,比如常见的404等等,这里后台给我规定了只要判断200,401和其他。
- 200表示请求成功,前端不用做什么多余的处理,直接获取数据解析即可;
- 401是后台规定返回给我的,可能每个项目后台返回的不一样,这里后台是可以控制的,具体可能不一样,但是影响不大。当出现这种情况时,我们前端需要做的事情就比较多了,从拦截的响应体中解析,注意,就算报401,后台也会返回给我数据的,比如下面是我们返回的json数据;
- 只要获取到响应json数据,然后解析,判断之类的就是了,这里比较简单;
- 然后其他的万一304、500那些直接弹出Toast。
现在只剩下一个重要问题,就是我们怎么知道这接口啥时候是200,啥时候是401,我们先假设在最后返回中进行判断,比如假设是OKHTTP,那么就是在onResponse中,我们需要先拿到RresponseCode,然后在根据不同的code,写不同的bean做解析,如果是用了Retrofit,这太不符合实际项目情况了,先不说我们项目这中网络请求框架封装的死死的,一般来说直接写好bean,直接就解析出bean了,这里效率慢了太多了,因为我们后面如果在401的情况下,再解析status状态码,如果需要刷新token,又要请求接口,所以这里我们需要用拦截器。
拦截器在我使用以后越来越感觉太强大了,首先它既可以拦截请求数据,比如请求行,请求头,请求体,同理也可以拦截响应行,响应头,响应体也可以获取到,最主要的是我们可以操作Request和Response。
用代码说话,先试用下对okhttpclient配置。
.addInterceptor(new TokenInterceptor()) //添加token拦截器
实现Interceptor接口,在intercept方法中写上如下代码。
Request request = chain.request(); long t1 = System.nanoTime(); LogUtils.logd(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime();
LogUtils.logd(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); LogUtils.logd("---- response code --- :"+response.code()); return response;
这样子基本可以将请求数据和响应数据都打印出来。所以这就是拦截器,那么我们要达到需要的结果逻辑就是Request先不拦截,拦截每一个请求的响应,然后对响应行中的响应码进行判断,如果是200,则不用管,直接返回该Response给前端,如果是401,则拦截响应体,解析响应体,然后通过后台自己的规则 进行判断,其中我们对status分为两类,一种是402 403 405 406,一种是404,前者所要做的逻辑操作都是一样,Toast message,然后退出登录,回到登录页面,后者因为token过期,需要请求刷新token接口获取新的token,再次发送请求。
第一类以后再说,因为不可能会是在拦截器中做操作的,先看第二类。
首先先拦截到响应码。
Request request = chain.request(); // try the request Response originalResponse = chain.proceed(request); /**通过如下的办法曲线取到请求完成的数据 * * 原本想通过 originalResponse.body().string() * 去取到请求完成的数据,但是一直报错,不知道是okhttp的bug还是操作不当 * * 然后去看了okhttp的源码,找到了这个曲线方法,取到请求完成的数据后,根据特定的判断条件去判断token过期 */ ResponseBody responseBody = originalResponse.body(); //首先从response中获取响应码,只判断 401 或 200 int responseCode = originalResponse.code();
获取到响应体的json字符串,也就是responseBody字符串,获取到它以后才能解析
BufferedSource source = responseBody.source(); source.request(Long.MAX_VALUE); // Buffer the entire body. Buffer buffer = source.buffer(); Charset charset = UTF8; MediaType contentType = responseBody.contentType(); if (contentType != null) { charset = contentType.charset(UTF8); } //获取响应体的字符串 String bodyString = buffer.clone().readString(charset); LogUtils.logd("body---------->" + bodyString);
解析
//将字符串解析成bean,然后判断bean里面的字段status TokenResponse responseB = (TokenResponse) JsonUtils.fromJson(bodyString, TokenResponse.class); int status = responseB.getStatus(); String message = responseB.getMessege();
对status进行判断
if(status == 402 || status ==403 || status==405 || status ==406){ LogUtils.logd("status:"+status+" message: "+message); return originalResponse; }else if(status == 404){ //token有效期过了,需重新刷新token //取出本地的refreshToken String refreshToken = SPUtils.getSharedStringData(BaseApplication.getAppContext(), AppConstant.REFRESH_TOKEN_KEY); // HashMap<String, Object> params = new HashMap<>(); params.put("refresh_token", refreshToken); String jsonString = JsonUtils.toJson(params); RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),jsonString); // 通过一个特定的接口获取新的token,此处要用到同步的retrofit请求 Call<RefreshTokenBean> call = Api.getApiService().refreshToken(Api.getCacheControl(),body); //要用retrofit的同步方式 RefreshTokenBean bean= call.execute().body(); LogUtils.logd("刷新token获取的新bean "+bean.toString()); //将新的数据用sp更新下保存起来 SPUtils.setSharedStringData(BaseApplication.getAppContext(),AppConstant.ACCESS_TOKEN_KEY,bean.getAccess_token()); SPUtils.setSharedStringData(BaseApplication.getAppContext(),AppConstant.TOKEN_TYPE_KEY,bean.getToken_type()); SPUtils.setSharedStringData(BaseApplication.getAppContext(),AppConstant.USER_ID,bean.getUserinfoId()); String newToken = SPUtils.getSharedStringData(BaseApplication.getAppContext(),AppConstant.ACCESS_TOKEN_KEY); // create a new request and modify it accordingly using the new token Request newRequest = request.newBuilder().header("Authorization",SPUtils.getSharedStringData(BaseApplication.getAppContext(),AppConstant.TOKEN_TYPE_KEY)+" "+newToken) .build(); originalResponse.body().close(); return chain.proceed(newRequest); }
上面这部分代码,可能每个人请求不一样,有点点细微差别,但是理解起来就是,如果我们不想管,那就从拦截器中获取到request,以此request返回response即可。
Request request = chain.request();
如果我们觉得该request不对,需要修改,那么就修改,然后用新的request返回新的resonse即可
Request newRequest = request.newBuilder().header("Authorization",SPUtils.getSharedStringData(BaseApplication.getAppContext(),AppConstant.TOKEN_TYPE_KEY)+" "+newToken) .build();
对于其他的status操作,需要退出登录,回到登录页面的,这里我使用的是retrofit,开始我也不知道怎样获取请求失败的responseBody,然后去J神的Github上搜了下,还真搜到了Retrofit ,在onError方法中竟然可以获取到responseBody,而且J神也提供了方法(可能因为版本不同 方法不一样)
HttpException error = (HttpException)e; String errorBody = ""; try { errorBody = error.response().errorBody().string(); } catch (IOException e1) { e1.printStackTrace(); }
嗯哼,这样子就获取失败的响应,接下来比较简单,大致差不多 清除SP数据 Toast message finish activity , initent login activity。
3.道友留步
这个项目命运多舛,一系列原因导致当初我搭建框架时是很久以前,当初拦截器那里一直想要获取responseBody的String方法,也就是将responseBody转成字符串,找了很多资料,很多方法都不行,然后在一位大神那里学到了下面的方法,挺感谢他的,但是时间太久找不到来源了。
BufferedSource source = responseBody.source(); source.request(Long.MAX_VALUE); // Buffer the entire body. Buffer buffer = source.buffer(); Charset charset = UTF8; MediaType contentType = responseBody.contentType(); if (contentType != null) { charset = contentType.charset(UTF8); }
希望能帮到你吧!!