42 votes

Okhttp rafraîchit le jeton expiré lors de l'envoi de plusieurs requêtes au serveur

J'ai un ViewPager et trois appels au service web sont effectués lorsque le ViewPager est chargé simultanément.

Lorsque le premier renvoie 401, l'Authenticator est appelé et je rafraîchis le jeton à l'intérieur de l'Authenticator, mais les 2 requêtes restantes sont déjà envoyées au serveur avec l'ancien jeton de rafraîchissement et échouent avec 498, ce qui est capturé dans l'intercepteur et l'application est déconnectée.

Ce n'est pas le comportement idéal que je m'attendrais. Je voudrais garder la 2e et la 3e requête dans la file d'attente et lorsque le jeton est rafraîchi, réessayer la requête mise en attente.

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

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

MODIFIER : Le problème que je veux résoudre est général et je ne souhaite pas séquencer mes appels, c'est-à-dire attendre qu'un appel se termine pour rafraîchir le jeton, puis envoyer le reste de la demande au niveau de l'activité et du fragment.

Le code a été demandé. Voici un code standard pour l'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 {

        // Invalider le jeton d'authentification
        String accessToken = accountManager.peekAuthToken(account, authTokenType);
        if (accessToken != null) {
            accountManager.invalidateAuthToken(accountType, accessToken);
        }
        try {
                // Obtenir un nouveau jeton de rafraîchissement. Cela invoque l'Authentificateur de compte personnalisé qui effectue un appel pour obtenir un nouveau jeton de rafraîchissement.
                accessToken = accountManager.blockingGetAuthToken(account, authTokenType, false);
                if (accessToken != null) {
                    Request.Builder requestBuilder = response.request().newBuilder();

                    // Ajouter des en-têtes avec le nouveau jeton de rafraîchissement

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

Quelques questions similaires à celle-ci : OkHttp and Retrofit, refresh token with concurrent requests

0 votes

Veuillez poster du code

0 votes

Pouvez-vous poster votre code d'authentification ? Merci. De plus, pourquoi obtenez-vous 498 de votre API avec votre jeton expiré ?

0 votes

@savepopulation 498 signifie Jeton invalide. Les 2 demandes qui ont été envoyées avec la première demande ont un jeton obsolète et la demande échoue avec le code d'erreur 498.

17voto

David Medenjak Points 4553

Il est important de noter que accountManager.blockingGetAuthToken (ou la version non bloquante) pourrait toujours être appelé ailleurs, autre que dans l'intercepteur. Par conséquent, le bon endroit pour empêcher que ce problème ne se produise serait au sein de l'authentificateur.

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


L'échantillon suivant se compose de 3 blocs.

Le premier concerne le fait de s'assurer qu'un seul thread récupère le jeton d'accès tandis que les autres threads inscrivent simplement leur response pour un rappel.

Le deuxième est simplement un paquet de résultats fictif vide. Ici, vous chargeriez votre jeton, le rafraîchiriez éventuellement, etc.

Le tiers est ce que vous faites une fois que vous avez votre résultat (ou une erreur). Vous devez vous assurer d'appeler la réponse pour chaque autre thread qui aurait pu s'inscrire.

boolean fetchingToken;
List queue = null;

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

  synchronized (this) {
    if (fetchingToken) {
      // un autre thread est déjà en train de travailler dessus, inscrivez-vous pour un rappel
      List q = queue;
      if (q == null) {
        q = new ArrayList<>();
        queue = q;
      }
      q.add(response);
      // nous retournons null, le résultat sera envoyé avec la `response`
      return null;
    }
    // nous devons récupérer le jeton, et renvoyer le résultat aux autres threads
    fetchingToken = true;
  }

  // charger le jeton d'accès, le rafraîchir avec le jeton de rafraîchissement, etc.
  // ... à faire ...
  Bundle result = Bundle.EMPTY;

  // boucle pour s'assurer que nous ne abandonnons aucune réponse
  for ( ; ; ) {
    List q;
    synchronized (this) {
      // obtenir la liste des réponses en attente de résultat
      q = queue;
      if (q == null) {
        fetchingToken = false;
        // nous avons terminé, personne n'attend de réponse, retourner
        return null;
      }
      queue = null;
    }

    // informer les autres threads du résultat
    for (AccountAuthenticatorResponse r : q) {
      r.onResult(result); // retourner le résultat
    }

    // répéter pour le cas où un autre thread s'est inscrit pour un rappel
    // pendant que nous étions occupés à appeler les autres
  }
}

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

Vous pourriez évidemment utiliser d'autres moyens pour synchroniser ces blocs de code, comme les atomiques comme le montre @matrix dans une autre réponse. J'ai utilisé synchronized, car je pense que c'est l'implémentation la plus facile à comprendre, puisque c'est une excellente question et que tout le monde devrait le faire ;)


L'échantillon ci-dessus est une version adaptée d'une boucle émettrice décrite ici, qui va en détail sur la concurrence. Ce blog est une excellente source si vous êtes intéressé par le fonctionnement de RxJava sous le capot.

0 votes

En jouant avec ceci, je vois que fetchingToken n'est jamais vrai. Chaque appel à getAuthToken parcourt simplement toute la méthode. Est-ce que j'ai oublié quelque chose ?

0 votes

@Jack Sauf si vous avez commis une erreur lors de sa mise en œuvre, cela signifie simplement que vous n'avez rencontré aucune condition de concurrence. Ce code garantit que SI plusieurs threads ont besoin d'un nouveau jeton d'accès, un seul le demande et transmet les résultats aux autres. J'ai pu tester cela en invalidant le jeton d'accès avant de lancer plus de 10 appels API en même temps sur des threads en arrière-plan. Ensuite, un thread effectuera la recherche, tandis que les autres attendront le résultat.

0 votes

Merci de me répondre. C'est ce que je pensais. J'appelle getAuthToken() à partir d'un Authenticator okhttp. J'utilise RxJava et je lance toutes les requêtes originales sur le thread io qui finit par appeler getAuthToken apparemment pas de la manière dont je m'y attendrais pour la synchronisation dans la méthode. Je suis encore assez nouveau en synchronisation alors je travaille toujours dessus. Si la méthode synchronisée est appelée à partir du même thread, attendrait-elle toujours?

16voto

matrix Points 354

Vous pouvez faire ceci :

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

// ces deux variables statiques servent au modèle pour rafraîchir un jeton
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);

et ensuite dans la méthode intercept :

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

        // 1. signer cette requête
        ....

        // 2. continuer avec la requête
        Response response = chain.proceed(request);

        // 3. vérifier la réponse : avons-nous reçu un 401 ?
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

            if (!TextUtils.isEmpty(token)) {
                /*
                *  Parce que nous envoyons plusieurs requêtes HTTP en parallèle, elles pourraient toutes indiquer un 401 en même temps.
                *  Une seule d'entre elles devrait rafraîchir le jeton, car sinon nous rafraîchirions le même jeton plusieurs fois
                *  et ce n'est pas bon. C'est pourquoi nous avons ces deux objets statiques, un ConditionVariable et un boolean. Le
                *  premier thread qui arrive ici ferme le ConditionVariable et change le drapeau boolean.
                */
                if (mIsRefreshing.compareAndSet(false, true)) {
                    LOCK.close();

                    /* nous sommes les premiers ici. rafraîchissons ce jeton.
                    *  il semble que notre jeton ne soit plus valide.
                    *  RAFRAÎCHISSEZ le jeton actuel ici
                    */

                    LOCK.open();
                    mIsRefreshing.set(false);
                } else {
                    // Un autre thread est en train de rafraîchir le jeton pour nous, attendons.
                    boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);

                    // Si la vérification suivante est fausse, cela signifie que le délai d'expiration est écoulé, c'est-à-dire que le rafraîchissement
                    // a échoué.
                    if (conditionOpened) {

                        // un autre thread l'a rafraîchi pour nous ! merci !
                        // signer la demande avec le nouveau jeton et continuer
                        // retourner le résultat de la demande nouvellement signée
                        response = chain.proceed(newRequest);
                    }
                }
            }
        }

        // vérifier si toujours non autorisé (c'est-à-dire si le rafraîchissement a échoué)
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
            ... // nettoyez votre jeton d'accès et demandez à nouveau la requête.
        }

        // retourner la réponse à la demande d'origine
        return response;
    }

De cette façon, vous n'enverrez qu'une seule requête pour rafraîchir le jeton, puis pour chaque autre vous aurez le jeton rafraîchi.

6voto

PN10 Points 1339

Vous pouvez essayer avec cet intercepteur au niveau de l'application

 private class HttpInterceptor implements Interceptor {

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

        //Construire une nouvelle demande
        Request.Builder builder = request.newBuilder();
        builder.header("Accept", "application/json"); //si nécessaire, indiquez la consommation de JSON

        String token = settings.getAccessToken(); //sauvegarder le jeton de cette demande pour l'avenir
        setAuthHeader(builder, token); //écrire le jeton actuel à la demande

        request = builder.build(); //écraser l'ancienne demande
        Response response = chain.proceed(request); //effectuer la demande, ici la demande d'origine sera exécutée

        if (response.code() == 401) { //si non autorisé
            synchronized (httpClient) { //effectuer tous les 401 dans des blocs synchronisés, pour éviter les mises à jour de jeton multiples
                String currentToken = settings.getAccessToken(); //obtenir le jeton actuellement stocké

                if(currentToken != null && currentToken.equals(token)) { //comparer le jeton actuel avec le jeton qui a été stocké auparavant, s'il n'a pas été mis à jour - mettre à jour

                    int code = refreshToken() / 100; //rafraîchir le jeton
                    if(code != 2) { //si le rafraîchissement du jeton a échoué pour une raison quelconque
                        if(code == 4) //uniquement si la réponse est 400, 500 pourrait signifier que le jeton n'a pas été mis à jour
                            logout(); //aller à l'écran de connexion
                        return response; //si la mise à jour du jeton a échoué - afficher une erreur à l'utilisateur
                    }
                }

                if(settings.getAccessToken() != null) { //la nouvelle tentative nécessite un nouveau jeton d'authentification,
                    setAuthHeader(builder, settings.getAccessToken()); //définir le jeton d'authentification mis à jour
                    request = builder.build();
                    return chain.proceed(request); //répéter la demande avec un nouveau jeton
                }
            }
        }

        return response;
    }

    private void setAuthHeader(Request.Builder builder, String token) {
        if (token != null) //Ajouter le jeton d'authentification à chaque demande si autorisé
            builder.header("Authorization", String.format("Bearer %s", token));
    }

    private int refreshToken() {
        //Rafraîchir le jeton, de manière synchrone, le sauvegarder et renvoyer le code de résultat
        //vous pourriez utiliser retrofit ici
    }

    private int logout() {
        //déconnectez votre utilisateur
    }
}

Vous pouvez définir un intercepteur comme ceci pour une instance 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 vous aidera!!!!

0voto

Genaro Nuño Points 1

J'ai trouvé la solution avec un authentificateur, l'identifiant est le numéro de la demande, seulement 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("Erreur" , "Une erreur 401 non autorisée a été trouvée et je suis le numéro : " + id);

                //Obtenir le jeton de la base de données
                SharedPreferences prefs = mContext.getSharedPreferences(
                        BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

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

                //Comparer les jetons
                if(mToken.getRefreshToken().equals(token_db)){

                    locks.lock(); 

                    try{
                        //Obtenir le jeton de la base de données
                         prefs = mContext.getSharedPreferences(
                                BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

                        String token_db2 = prefs.getString("refresh_token","");
                        //Comparer les jetons
                        if(mToken.getRefreshToken().equals(token_db2)){

                            //Actualiser le jeton
                            APIClient tokenClient = createService(APIClient.class);
                            Call call = tokenClient.getRefreshAccessToken(API_OAUTH_CLIENTID,API_OAUTH_CLIENTSECRET, "refresh_token", mToken.getRefreshToken());
                            retrofit2.Response res = call.execute();
                            AccessToken newToken = res.body();
                            // avons-nous un jeton d'accès à actualiser?
                            if(newToken!=null && res.isSuccessful()){
                                String refreshToken = newToken.getRefreshToken();

                                    Log.e("Entrer", "Jeton actualisé et je suis le numéro :  " + 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{
                                //Rediriger vers la connexion
                                Log.e("rediriger", "REDIRECTION VERS LA DÉCONNEXION");

                                locks.unlock();
                                return null;
                            }

                        }else{
                            //Les jetons ont déjà été mis à jour

                            Log.e("Entrer", "Le jeton a déjà été mis à jour précédemment, et je suis le numéro : " + 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;
            }
        });

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X