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); }
希望能幫到你吧!!