Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Retrofit Token過期自動刷新並重新請求接口

Retrofit Token過期自動刷新並重新請求接口

編輯:關於Android編程

在有心課堂的群裡,有網友提出如下場景:

當前開發的 App 遇到一個問題:

當請求某個接口時,由於 token 已經失效,所以接口會報錯。
但是產品經理希望 app 能夠馬上刷新 token ,然後重復請求剛才那個接口,這個過程對用戶來說是無感的。

請求 A 接口-》服務器返回 token 過期-》請求 token 刷新接口-》請求 A 接口

我們應該是怎麼解決這個問題呢?

經過百度搜索到了相關信息,這裡總結下。

本文是采用RxJava + Retrofit來實現網絡請求封裝。

實現原理

利用 Observale 的 retryWhen 的方法,識別 token 過期失效的錯誤信息,此時發出刷新 token 請求的代碼塊,完成之後更新 token,這時之前的請求會重新執行,但將它的 token 更新為最新的。另外通過代理類對所有的請求都進行處理,完成之後,我們只需關注單個 API 的實現,而不用每個都考慮 token 過期,大大地實現解耦操作。

App多個請求token失效的處理邏輯

當集成了Retrofit之後,我們app中的網絡請求接口則變成了一個個單獨的方法,這時我們需要添加一個全局的token錯誤拋出機制,來避免每個接口都所需要的token驗證處理。

token失效錯誤拋出

在Retrofit中的Builder中,是通過GsonConvertFactory來做json轉成model數據處理的,這裡我們就需要重新實現一個自己的GsonConvertFactory,這裡主要由三個文件GsonConvertFactory,GsonRequestBodyConverter,GsonResponseBodyConverter,它們三個從源碼中拿過來新建即可。主要我們重寫GsonResponseBodyConverter這個類中的convert的方法,這個方法主要將ResponseBody轉換我們需要的Object,這裡我們通過拿到我們的token失效的錯誤信息,然後將其以一個指定的Exception的信息拋出。

GsonConverterFactory代碼如下:

這裡寫圖片描述

修改的地方:
1.修改 GsonConverterFactory 中,生成 GsonResponseBodyConverter 的方法:

@Override
public Converter responseBodyConverter(final Type type, Annotation[] annotations, Retrofit retrofit) {
  Type newType = new ParameterizedType() {
      @Override
      public Type[] getActualTypeArguments() {
          return new Type[] { type };
      }

      @Override
      public Type getOwnerType() {
          return null;
      }

      @Override
      public Type getRawType() {
          return ApiModel.class;
      }
  };
  TypeAdapter adapter = gson.getAdapter(TypeToken.get(newType));
  return new GsonResponseBodyConverter<>(adapter);
}

可以看出我們這裡對 type 類型,做以包裝,讓其重新生成一個類型為 ApiModel 的新類型。因為我們在寫接口代碼的時候,都以真正的類型 type 來作為返回值的,而不是 ApiModel。

2.GsonResponseBodyConverter的處理 它的修改,則是要針對返回結果,做以異常的判斷並拋出,主要看其的 convert方法:

@Override
public Object convert(ResponseBody value) throws IOException {
  try {
      ApiModel apiModel = (ApiModel) adapter.fromJson(value.charStream());
      if (apiModel.errorCode == ErrorCode.TOKEN_NOT_EXIST) {
          throw new TokenNotExistException();
      } else if (apiModel.errorCode == ErrorCode.TOKEN_INVALID) {
          throw new TokenInvalidException();
      } else if (!apiModel.success) {
          // TODO: 16/8/21 handle the other error.
          return null;
      } else if (apiModel.success) {
          return apiModel.data;
      }
  } finally {
      value.close();
  }
  return null;
}

錯誤拋出

當服務器錯誤信息的時候,同樣也是一個 model,不同的是 success 為 false,並且含有 error_code的信息。所以我們需要針對 model 處理的時候,做以判斷。主要修改的地方就是 retrofit 的 GsonConvertFactory,這裡不再通過 gradle 引入,直接把其源碼中的三個文件添加到咱們的項目中。

首先提及的一下是對統一 model 的封裝,如下:

public class ApiModel {
    public boolean success;
    @SerializedName("error_code") public int errorCode;

    public T data;
}

當正確返回的時候,我們獲取到 data,直接給上層;當出錯的時候,可以針對 errorCode的信息,做一些處理,讓其走最上層調用的 onError 方法。

多請求的API代理

為所有的請求都添加Token的錯誤驗證,還要做統一的處理。借鑒Retrofit創建接口的api,我們也采用代理類,來對Retrofit的API做統一的代理處理。

建立API代理類

public class ApiServiceProxy {

    Retrofit mRetrofit;

    ProxyHandler mProxyHandler;

    public ApiServiceProxy(Retrofit retrofit, ProxyHandler proxyHandler) {
        mRetrofit = retrofit;
        mProxyHandler = proxyHandler;
    }

    public  T getProxy(Class tClass) {
        T t = mRetrofit.create(tClass);
        mProxyHandler.setObject(t);
        return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[] { tClass }, mProxyHandler);
    }
}

這樣,我們就需要通過ApiServiceProxy中的getProxy方法來創建API請求。另外,其中的ProxyHandler則是實現InvocationHandler來實現。

public class ProxyHandler implements InvocationHandler {

    private Object mObject;

    public void setObject(Object obj) {
        this.mObject = obj;
    }

    @Override
    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
        Object result = null;
        result = Observable.just(null)
            .flatMap(new Func1

這裡的invoke方法則是我們的重頭戲,在其中通過將method.invoke方法包裝在Observable中,並添加retryWhen的方法,在retryWhen方法中,則對我們在GsonResponseBodyConverter中暴露出來的錯誤,做一判斷,然後執行重新獲取token的操作,這段代碼就很簡單了。就不再這裡細述了。

還有一個重要的地方就是,當token刷新成功之後,我們將舊的token替換掉呢?筆者查了一下,java8中的method類,已經支持了動態獲取方法名稱,而之前的Java版本則是不支持的。那這裡怎麼辦呢?通過看retrofit的調用,可以知道retrofit是可以將接口中的方法轉換成API請求,並需要封裝參數的。那就需要看一下Retrofit是如何實現的呢?最後發現重頭戲是在Retrofit對每個方法添加的@interface的注解,通過Method類中的getParameterAnnotations來進行獲取,主要的代碼實現如下:

/**
     * Update the token of the args in the method.
     */
private void updateMethodToken(Method method, Object[] args) {
        if (mIsTokenNeedRefresh && !TextUtils.isEmpty(GlobalToken.getToken())) {
            Annotation[][] annotationsArray = method.getParameterAnnotations();
            Annotation[] annotations;
            if (annotationsArray != null && annotationsArray.length > 0) {
                for (int i = 0; i < annotationsArray.length; i++) {
                    annotations = annotationsArray[i];
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Query) {
                            if (TOKEN.equals(((Query) annotation).value())) {
                                args[i] = GlobalToken.getToken();
                            }
                        }
                    }
                }
            }
            mIsTokenNeedRefresh = false;
        }
    }

這裡,則遍歷我們所使用的token字段,然後將其替換成新的token.

代碼驗證

最上層的代碼調用中,添加了兩個按鈕:

按鈕1:獲取token

這裡寫圖片描述

token 獲取成功之後,僅僅更新一下全局的token即可。

按鈕2:正常的請求

這裡為了模擬多請求,這裡我直接調正常的請求5次:
這裡寫圖片描述

為了查看輸出,另外對 Okhttp 添加了 HttpLoggingInterceptor 並設置 Body 的 level 輸出,用來監測 http 請求的輸出。

一切完成之後,先點擊獲取 token 的按鈕,等待30秒之後,再點擊正常請求按鈕。可以看到如下的輸出:

--> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (8ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (5ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 --> GET http://192.168.56.1:8888/refresh_token http/1.1
 --> END GET
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 {"success":false,"error_code":1001}
 Transfer-Encoding: chunked
 <-- END HTTP (35-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/refresh_token (2ms)
 Content-Type: text/plain
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (6ms)
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Connection: keep-alive
 Transfer-Encoding: chunked
 Transfer-Encoding: chunked
 {"success":true,"data":{"token":"1471826289336"}}
 <-- END HTTP (49-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
roxy: Refresh token success, time = 1471790019657
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (2ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (6ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)

剛發出的5個請求都返回了 token 過期的 error,之後看到一個重新刷新 token 的請求,它成功之後,原先的5個請求又進行了重試,並都返回了成功的信息。

完整代碼:
AndroidDemos/tree/master/app/src/main/java/com/lighters/demos/token">https://github.com/alighters/AndroidDemos/tree/master/app/src/main/java/com/lighters/demos/token

server代碼則是根目錄下的 server 文件夾中,測試的時候不要忘啟動 server 哦。


以上實現是將token放在在url裡面,如果是放在Header裡面,怎麼實現呢?還是要通過okhttp的攔截器來實現。

思路:
1.通過攔截器,獲取返回的數據
2.判斷token是否過期
3.如果token過期則刷新token
4.使用最新的token,重新請求網絡數據

實現如下:

public class TokenInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        if (isTokenExpired(response)) {//根據和服務端的約定判斷token過期
            //同步請求方式,獲取最新的Token
            String newSession = getNewToken();
            //使用新的Token,創建新的請求
            Request newRequest = chain.request()
                    .newBuilder()
                    .header("Cookie", "JSESSIONID=" + newSession)
                    .build();
            //重新請求
            return chain.proceed(newRequest);
        }
        return response;
    }

    /**
     * 根據Response,判斷Token是否失效
     *
     * @param response
     * @return
     */
    private boolean isTokenExpired(Response response) {
        if (response.code() == 404) {
            return true;
        }
        return false;
    }

    /**
     * 同步請求方式,獲取最新的Token
     *
     * @return
     */
    private String getNewToken() throws IOException {
        // 通過一個特定的接口獲取新的token,此處要用到同步的retrofit請求
        Response_Login loginInfo = CacheManager.restoreLoginInfo(BaseApplication.getContext());
        String username = loginInfo.getUserName();
        String password = loginInfo.getPassword();

        Call call = WebHelper.getSyncInterface().synclogin(new Request_Login(username, password));
        loginInfo = call.execute().body();

        loginInfo.setPassword(password);
        CacheManager.saveLoginInfo(loginInfo);
        return loginInfo.getSession();
    }
}

添加攔截器:

OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new TokenInterceptor())
                .build();
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved