web-dev-qa-db-ja.com

複数のリクエストがサーバーに送信された場合のOkhttpリフレッシュ期限切れトークン

ViewPagerがあり、ViewPagerが同時にロードされると3つのWebサービス呼び出しが行われます。

最初のリクエストが401を返すと、Authenticatorが呼び出され、Authenticator内のトークンを更新しますが、残りの2つのリクエストは古いリフレッシュトークンで既にサーバーに送信され、Interceptorでキャプチャされた498で失敗しますアプリがログアウトされます。

これは私が期待する理想的な動作ではありません。 2番目と3番目の要求をキューに保持し、トークンが更新されたら、キューに入れられた要求を再試行します。

現在、Authenticatorでトークンの更新が進行中かどうかを示す変数があります。その場合、Interceptorの後続のすべてのリクエストをキャンセルし、ユーザーはページを手動で更新するか、ログアウトできますユーザーとユーザーのログインを強制します。

Androidのokhttp 3.xを使用して上記の問題を解決するのに適したソリューションまたはアーキテクチャは何ですか?

編集:私が解決したい問題は一般的であり、私は私の呼び出しをシーケンスしたくない。つまり、1つの呼び出しが終了してトークンを更新するのを待ってから、アクティビティレベルとフラグメントレベルで残りのリクエストのみを送信します。

コードが要求されました。これはAuthenticatorの標準コードです:

public class CustomAuthenticator implements Authenticator {

    @Inject AccountManager accountManager;
    @Inject @AccountType String accountType;
    @Inject @AuthTokenType String authTokenType;

    @Inject
    public ApiAuthenticator(@ForApplication Context context) {
    }

    @Override
    public Request authenticate(Route route, Response response) throws IOException {

        // Invaidate authToken
        String accessToken = accountManager.peekAuthToken(account, authTokenType);
        if (accessToken != null) {
            accountManager.invalidateAuthToken(accountType, accessToken);
        }
        try {
                // Get new refresh token. This invokes custom AccountAuthenticator which makes a call to get new refresh token.
                accessToken = accountManager.blockingGetAuthToken(account, authTokenType, false);
                if (accessToken != null) {
                    Request.Builder requestBuilder = response.request().newBuilder();

                    // Add headers with new refreshToken

                    return requestBuilder.build();
            } catch (Throwable t) {
                Timber.e(t, t.getLocalizedMessage());
            }
        }
        return null;
    }
}

これに似たいくつかの質問: OkHttp and Retrofit、refresh token with concurrent requests

30
sat

accountManager.blockingGetAuthToken(または非ブロッキングバージョン)は、インターセプター以外の場所で呼び出すことができます。したがって、この問題が発生するのを防ぐ正しい場所は、認証子内のです。

アクセストークンを必要とする最初のスレッドがそれを取得することを確認し、最初のスレッドがトークンの取得を完了したときに呼び出されるコールバックに他のスレッドを登録する必要があります。
朗報は、AbstractAccountAuthenticatorには既に非同期結果、つまりAccountAuthenticatorResponseを提供する方法があり、onResultまたはonError


次のサンプルは3つのブロックで構成されています。

first1つは、1つのスレッドのみがアクセストークンをフェッチし、他のスレッドはコールバック用にresponseを登録するだけです。

second部分は単なる空の結果バンドルです。ここで、トークンをロードしたり、場合によっては更新したりします。

thirdの部分は、結果(またはエラー)を取得した後に行うことです。登録されている可能性のある他のすべてのスレッドの応答を必ず呼び出す必要があります。

boolean fetchingToken;
List<AccountAuthenticatorResponse> queue = null;

@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {

  synchronized (this) {
    if (fetchingToken) {
      // another thread is already working on it, register for callback
      List<AccountAuthenticatorResponse> q = queue;
      if (q == null) {
        q = new ArrayList<>();
        queue = q;
      }
      q.add(response);
      // we return null, the result will be sent with the `response`
      return null;
    }
    // we have to fetch the token, and return the result other threads
    fetchingToken = true;
  }

  // load access token, refresh with refresh token, whatever
  // ... todo ...
  Bundle result = Bundle.EMPTY;

  // loop to make sure we don't drop any responses
  for ( ; ; ) {
    List<AccountAuthenticatorResponse> q;
    synchronized (this) {
      // get list with responses waiting for result
      q = queue;
      if (q == null) {
        fetchingToken = false;
        // we're done, nobody is waiting for a response, return
        return null;
      }
      queue = null;
    }

    // inform other threads about the result
    for (AccountAuthenticatorResponse r : q) {
      r.onResult(result); // return result
    }

    // repeat for the case another thread registered for callback
    // while we were busy calling others
  }
}

nullを使用する場合は、必ずすべてのパスでresponseを返すようにしてください。

別の応答で@matrixが示すアトミックなど、他の手段を使用してこれらのコードブロックを同期することは明らかです。 synchronizedを利用しました。これは実装を把握するのが最も簡単だと信じているからです。


上記のサンプルは、 ここで説明するエミッターループ の適合バージョンで、同時実行性について詳しく説明しています。 RxJavaが内部でどのように機能するかに興味がある場合、このブログは素晴らしい情報源です。

12
David Medenjak

あなたはこれを行うことができます:

それらをデータメンバーとして追加します。

// these two static variables serve for the pattern to refresh a token
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);

そして、インターセプトメソッドで:

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

        // 1. sign this request
        ....

        // 2. proceed with the request
        Response response = chain.proceed(request);

        // 3. check the response: have we got a 401?
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

            if (!TextUtils.isEmpty(token)) {
                /*
                *  Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
                *  Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
                *  and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
                *  first thread that gets here closes the ConditionVariable and changes the boolean flag.
                */
                if (mIsRefreshing.compareAndSet(false, true)) {
                    LOCK.close();

                    /* we're the first here. let's refresh this token.
                    *  it looks like our token isn't valid anymore.
                    *  REFRESH the actual token here
                    */

                    LOCK.open();
                    mIsRefreshing.set(false);
                } else {
                    // Another thread is refreshing the token for us, let's wait for it.
                    boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);

                    // If the next check is false, it means that the timeout expired, that is - the refresh
                    // stuff has failed.
                    if (conditionOpened) {

                        // another thread has refreshed this for us! thanks!
                        // sign the request with the new token and proceed
                        // return the outcome of the newly signed request
                        response = chain.proceed(newRequest);
                    }
                }
            }
        }

        // check if still unauthorized (i.e. refresh failed)
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
            ... // clean your access token and Prompt for request again.
        }

        // returning the response to the original request
        return response;
    }

この方法では、トークンを更新するためのリクエストを1つだけ送信し、それ以外の場合は更新されたトークンを取得します。

8
matrix

このアプリケーションレベルのインターセプターで試すことができます

 private class HttpInterceptor implements Interceptor {

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

        //Build new request
        Request.Builder builder = request.newBuilder();
        builder.header("Accept", "application/json"); //if necessary, say to consume JSON

        String token = settings.getAccessToken(); //save token of this request for future
        setAuthHeader(builder, token); //write current token to request

        request = builder.build(); //overwrite old request
        Response response = chain.proceed(request); //perform request, here original request will be executed

        if (response.code() == 401) { //if unauthorized
            synchronized (httpClient) { //perform all 401 in sync blocks, to avoid multiply token updates
                String currentToken = settings.getAccessToken(); //get currently stored token

                if(currentToken != null && currentToken.equals(token)) { //compare current token with token that was stored before, if it was not updated - do update

                    int code = refreshToken() / 100; //refresh token
                    if(code != 2) { //if refresh token failed for some reason
                        if(code == 4) //only if response is 400, 500 might mean that token was not updated
                            logout(); //go to login screen
                        return response; //if token refresh failed - show error to user
                    }
                }

                if(settings.getAccessToken() != null) { //retry requires new auth token,
                    setAuthHeader(builder, settings.getAccessToken()); //set auth token to updated
                    request = builder.build();
                    return chain.proceed(request); //repeat request with new token
                }
            }
        }

        return response;
    }

    private void setAuthHeader(Request.Builder builder, String token) {
        if (token != null) //Add Auth token to each request if authorized
            builder.header("Authorization", String.format("Bearer %s", token));
    }

    private int refreshToken() {
        //Refresh token, synchronously, save it, and return result code
        //you might use retrofit here
    }

    private int logout() {
        //logout your user
    }
}

このようなインターセプターをokHttpインスタンスに設定できます

    Gson gson = new GsonBuilder().create();

    OkHttpClient httpClient = new OkHttpClient();
    httpClient.interceptors().add(new HttpInterceptor());

    final RestAdapter restAdapter = new RestAdapter.Builder()
            .setEndpoint(BuildConfig.REST_SERVICE_URL)
            .setClient(new OkClient(httpClient))
            .setConverter(new GsonConverter(gson))
            .setLogLevel(RestAdapter.LogLevel.BASIC)
            .build();

    remoteService = restAdapter.create(RemoteService.class);

お役に立てれば!!!!

2
PN10

私はオーセンティケーターで解決策を見つけました、idはリクエストの番号であり、識別のみを目的としています。コメントはスペイン語です

 private final static Lock locks = new ReentrantLock();

httpClient.authenticator(new Authenticator() {
            @Override
            public Request authenticate(@NonNull Route route,@NonNull Response response) throws IOException {

                Log.e("Error" , "Se encontro un 401 no autorizado y soy el numero : " + id);

                //Obteniendo token de DB
                SharedPreferences prefs = mContext.getSharedPreferences(
                        BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

                String token_db = prefs.getString("refresh_token","");

                //Comparando tokens
                if(mToken.getRefreshToken().equals(token_db)){

                    locks.lock(); 

                    try{
                        //Obteniendo token de DB
                         prefs = mContext.getSharedPreferences(
                                BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

                        String token_db2 = prefs.getString("refresh_token","");
                        //Comparando tokens
                        if(mToken.getRefreshToken().equals(token_db2)){

                            //Refresh token
                            APIClient tokenClient = createService(APIClient.class);
                            Call<AccessToken> call = tokenClient.getRefreshAccessToken(API_OAUTH_CLIENTID,API_OAUTH_CLIENTSECRET, "refresh_token", mToken.getRefreshToken());
                            retrofit2.Response<AccessToken> res = call.execute();
                            AccessToken newToken = res.body();
                            // do we have an access token to refresh?
                            if(newToken!=null && res.isSuccessful()){
                                String refreshToken = newToken.getRefreshToken();

                                    Log.e("Entra", "Token actualizado y soy el numero :  " + id + " : " + refreshToken);

                                    prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
                                    prefs.edit().putBoolean("log_in", true).apply();
                                    prefs.edit().putString("access_token", newToken.getAccessToken()).apply();
                                    prefs.edit().putString("refresh_token", refreshToken).apply();
                                    prefs.edit().putString("token_type", newToken.getTokenType()).apply();

                                    locks.unlock();

                                    return response.request().newBuilder()
                                            .header("Authorization", newToken.getTokenType() + " " + newToken.getAccessToken())
                                            .build();

                             }else{
                                //Dirigir a login
                                Log.e("redirigir", "DIRIGIENDO LOGOUT");

                                locks.unlock();
                                return null;
                            }

                        }else{
                            //Ya se actualizo tokens

                            Log.e("Entra", "El token se actualizo anteriormente, y soy el no : " + id );

                            prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

                            String type = prefs.getString("token_type","");
                            String access = prefs.getString("access_token","");

                            locks.unlock();

                            return response.request().newBuilder()
                                    .header("Authorization", type + " " + access)
                                    .build();
                        }

                    }catch (Exception e){
                        locks.unlock();
                        e.printStackTrace();
                        return null;
                    }


                }
                return null;
            }
        });
0
Genaro Nuño