it-swarm.com.de

Die Aktualisierung der Okhttp-Aktualisierung ist abgelaufen, wenn mehrere Anforderungen an den Server gesendet werden

Ich habe eine ViewPager und drei Webservice-Aufrufe werden ausgeführt, wenn ViewPager gleichzeitig geladen wird. 

Wenn zuerst 401 zurückgegeben wird, wird Authenticator aufgerufen und ich aktualisiere das Token in Authenticator, aber die verbleibenden 2 Anforderungen werden bereits mit dem alten Aktualisierungstoken an den Server gesendet und schlagen mit 498 fehl, das in Interceptor erfasst und die App abgemeldet wird. 

Dies ist nicht das ideale Verhalten, das ich erwarten würde. Ich möchte die 2. und 3. Anforderung in der Warteschlange behalten. Wenn das Token aktualisiert wird, versuchen Sie es erneut.

Derzeit habe ich eine Variable, um anzuzeigen, ob die Tokenaktualisierung in Authenticator ausgeführt wird. In diesem Fall stelle ich alle nachfolgenden Anforderungen in der Interceptor ab und der Benutzer muss die Seite manuell aktualisieren, oder ich kann den Benutzer abmelden und den Benutzer zur Anmeldung zwingen.

Was ist eine gute Lösung oder Architektur für das obige Problem mit okhttp 3.x für Android?

BEARBEITEN: Das Problem, das ich lösen möchte, ist im Allgemeinen und ich möchte meine Anrufe nicht sequenzieren. d. h. warten, bis ein Aufruf das Token beendet und aktualisiert hat und dann nur den Rest der Anforderung auf der Aktivitäts- und Fragmentebene sendet.

Code wurde angefordert. Dies ist ein Standardcode für 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;
    }
}

Einige ähnliche Fragen: OkHttp und Retrofit, Aktualisierungstoken mit gleichzeitigen Anforderungen

24
sat

Es ist wichtig anzumerken, dass accountManager.blockingGetAuthToken (oder die nicht-blockierende Version) immer noch an einem anderen Ort als dem Interceptor aufgerufen werden kann. Der richtige Ort, um zu verhindern, dass dieses Problem auftritt, wäre innerhalb der Authentifikator.

Wir möchten sicherstellen, dass der erste Thread, der ein Zugriffstoken benötigt, dieses abruft, und mögliche andere Threads sollten sich nur für einen Callback registrieren, der aufgerufen werden soll, wenn der erste Thread das Token abgerufen hat.
Die gute Nachricht ist, dass AbstractAccountAuthenticator bereits eine Möglichkeit hat, asynchrone Ergebnisse zu liefern, nämlich AccountAuthenticatorResponse, auf die Sie onResult oder onError aufrufen können.


Das folgende Beispiel besteht aus 3 Blöcken.

Bei first geht es darum sicherzustellen, dass nur ein Thread das Zugriffstoken abruft, während andere Threads ihre response nur für einen Rückruf registrieren.

Der second -Teil ist nur ein leeres Ergebnispaket. Hier würden Sie Ihr Token laden, möglicherweise aktualisieren usw.

Der dritte Teil ist das, was Sie tun, wenn Sie Ihr Ergebnis (oder einen Fehler) haben. Sie müssen sicherstellen, dass Sie die Antwort für jeden anderen Thread aufrufen, der möglicherweise registriert wurde.

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

Stellen Sie sicher, dass Sie in allen Pfaden null zurückgeben, wenn Sie die response verwenden.

Sie können natürlich andere Mittel verwenden, um diese Codeblöcke zu synchronisieren, z. B. Atomic, wie von @matrix in einer anderen Antwort gezeigt. Ich habe synchronized verwendet, weil ich glaube, dass dies die Implementierung ist, die am einfachsten zu verstehen ist, da dies eine großartige Frage ist und jeder dies tun sollte;)


Das obige Beispiel ist eine angepasste Version einer Emitter-Schleife, die hier beschrieben wird. Hier wird ausführlich auf Parallelität eingegangen. Dieser Blog ist eine großartige Quelle, wenn Sie daran interessiert sind, wie RxJava unter der Haube arbeitet.

10
David Medenjak

Du kannst das:

Fügen Sie diese als Datenmitglieder hinzu:

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

und dann zur Intercept-Methode:

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

Auf diese Weise senden Sie nur eine Anforderung, um das Token zu aktualisieren, und dann wird das Token für alle anderen aktualisiert.

7
matrix

Sie können es mit diesem Interceptor auf Anwendungsebene versuchen

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

Sie können den Interceptor wie folgt auf die okHttp-Instanz setzen

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

Hoffe das hilft!!!!

2
PN10

Ich habe die Lösung mit Authenticator gefunden, die ID ist die Nummer der Anfrage, nur zur Identifikation. Kommentare sind auf Spanisch

 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