web-dev-qa-db-fra.com

Jeton d'actualisation Okhttp expiré lorsque plusieurs demandes sont envoyées au serveur

J'ai une ViewPager et trois appels de service Web sont effectués lorsque ViewPager est chargé simultanément. 

Lorsque le premier retourne 401, Authenticator est appelé et j'actualise le jeton dans Authenticator, mais les 2 demandes restantes sont déjà envoyées au serveur avec l'ancien jeton d'actualisation et échouent avec 498 qui est capturé dans Interceptor et l'application est déconnectée. 

Ce n'est pas le comportement idéal auquel je m'attendrais. Je souhaite conserver la deuxième et la troisième demande dans la file d'attente et, lorsque le jeton est actualisé, réessayez la demande en file d'attente.

Actuellement, j'ai une variable pour indiquer si l'actualisation du jeton est en cours dans Authenticator. Dans ce cas, j'annule toutes les demandes ultérieures dans la Interceptor et l'utilisateur doit actualiser manuellement la page ou je peux le déconnecter et le forcer à se connecter.

Quelle est la bonne solution ou architecture pour le problème ci-dessus utilisant okhttp 3.x pour Android?

EDIT: Le problème que je veux résoudre est en général et je ne voudrais pas ordonner mes appels. c'est-à-dire attendre la fin d'un appel, actualiser le jeton, puis envoyer uniquement le reste de la demande au niveau de l'activité et du fragment.

Le code a été demandé. C'est un code standard pour 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;
    }
}

Quelques questions similaires à celles-ci: OkHttp et Retrofit, actualise le jeton avec les demandes simultanées

24
sat

Il est important de noter que accountManager.blockingGetAuthToken (ou la version non bloquante) peut toujours être appelé ailleurs, à l'exception de l'intercepteur. Par conséquent, le bon endroit pour empêcher ce problème de se produire serait intra l'authentificateur.

Nous voulons nous assurer que le premier thread qui a besoin d'un jeton le récupérera et que d'autres threads éventuels devraient simplement s'enregistrer pour qu'un rappel soit appelé lorsque le premier thread a fini de récupérer le jeton.
La bonne nouvelle est que AbstractAccountAuthenticator dispose déjà d'un moyen de fournir des résultats asynchrones, à savoir AccountAuthenticatorResponse, sur lequel vous pouvez appeler onResult ou onError.


L'exemple suivant est constitué de 3 blocs.

Le first consiste à s'assurer qu'un seul thread récupère le jeton d'accès, tandis que les autres threads n'enregistrent que leur response pour un rappel.

La partie second est simplement un paquet de résultats vide et factice. Ici, vous pouvez charger votre jeton, éventuellement le rafraîchir, etc.

La partie third est ce que vous faites une fois que vous avez votre résultat (ou erreur). Vous devez vous assurer d'appeler la réponse pour chaque autre thread susceptible de s'être enregistré.

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
  }
}

Assurez-vous simplement de renvoyer null sur tous les chemins lorsque vous utilisez response.

Vous pouvez évidemment utiliser d'autres moyens pour synchroniser ces blocs de code, comme atomics, comme le montre @matrix dans une autre réponse. Je me suis servi de synchronized, car j'estime qu'il est le plus facile à comprendre, car c'est une excellente question et que tout le monde devrait le faire;)


L'exemple ci-dessus est une version adaptée d'une boucle émetteur décrite ici , où il explique en détail la simultanéité. Ce blog est une excellente source si vous êtes intéressé par le fonctionnement de RxJava sous le capot.

10
David Medenjak

Tu peux le faire:

Ajoutez-les en tant que membres de données:

// 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);

puis sur la méthode d'interception:

@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;
    }

De cette façon, vous n’enverrez qu’une seule demande d’actualisation du jeton, puis vous obtiendrez l’autre pour chaque autre.

7
matrix

Vous pouvez essayer avec cet intercepteur de niveau d'application

 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
    }
}

Vous pouvez définir un intercepteur comme ceci sur 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);

J'espère que cela t'aides!!!!

2
PN10

J'ai trouvé la solution avec authentificateur, l'id est le numéro de la demande, uniquement pour l'identification. Les commentaires sont en espagnol

 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