2017-07-11 78 views
14

Mam ViewPager i trzy wywołania usługi sieciowej są wykonywane, gdy ViewPager jest ładowany jednocześnie.Okhttp odroczyć wygasły token, gdy wiele żądań jest wysyłanych na serwer

Kiedy pierwszy zwraca 401, Authenticator nazywa i odświeżyć token wewnątrz Authenticator, ale pozostałe 2 wnioski zostały już wysłane do serwera ze starego odświeżania żeton i nie powiedzie się z 498, które jest zrobione w Interceptor i aplikacja jest wylogowany.

Nie jest to idealne zachowanie, jakiego oczekiwałbym. Chciałbym zatrzymać drugie i trzecie żądanie w kolejce, a gdy token zostanie odświeżony, ponowić żądanie kolejkowania.

Obecnie mam zmienną wskazującą, czy odświeżenie tokena trwa w Authenticator, w tym przypadku anuluję wszystkie kolejne żądania w Interceptor i użytkownik musi ręcznie odświeżyć stronę lub mogę wylogować użytkownika i zmusić użytkownika do Zaloguj Się.

Co to jest dobre rozwiązanie lub architektura powyższego problemu za pomocą okhttp 3.x na Androida?

EDYCJA: Problem, który chcę rozwiązać, jest generalny i nie chciałbym sekwencjonować połączeń. tj. czekać na jedno połączenie, aby zakończyć i odświeżyć token, a następnie wysłać tylko resztę żądania na poziomie aktywności i fragmentu.

Żądano kodu. To Norma 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; 
    } 
} 

Niektóre pytania podobne do tego: OkHttp and Retrofit, refresh token with concurrent requests

+0

proszę napisać jakiś kod –

+0

czy możesz zamieścić swój kod uwierzytelniający? dzięki. także dlaczego otrzymujesz 498 z api z wygasłym tokenem? – savepopulation

+0

@suppopulation 498 oznacza Nieprawidłowy token. 2 żądania, które zostały wysłane razem z pierwszym żądaniem mają stary token i żądanie kończy się niepowodzeniem z 498 kodem błędu. – sat

Odpowiedz

6

Można to zrobić:

Dodaj tych członków danych:

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

i następnie w metodzie przechwytywania:

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

W ten sposób wyślemy tylko 1 wniosek o odświeżenie tokena, a następnie dla każdego innego otrzymasz odświeżony token.

2

Można spróbować z tego poziomu aplikacji przechwytujących

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

Można ustawić przechwytywania tak aby okHttp instancję

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

Hope this helps !!!!

8

Należy pamiętać, że accountManager.blockingGetAuthToken (lub wersja nieblokująca) może być nadal wywoływana w innym miejscu niż przechwytywacz. Dlatego właściwym miejscem, aby zapobiec temu problemowi, byłby w ramach numeru uwierzytelniającego.

Chcemy się upewnić, że pierwszy wątek wymagający tokena dostępu odzyska go, a ewentualne inne wątki powinny po prostu zarejestrować się, aby wywołanie było wywoływane po zakończeniu pierwszego wątku pobierania tokena.
Dobrą wiadomością jest to, że AbstractAccountAuthenticator ma już sposób dostarczania wyników asynchronicznych, a mianowicie AccountAuthenticatorResponse, pod którym można zadzwonić pod numer onResult lub onError.


Następująca próbka składa się z 3 bloków.

Najpierw należy się upewnić, że tylko jeden wątek pobiera token dostępu, podczas gdy inne wątki po prostu rejestrują swoje wywołania zwrotne.

Część druga to po prostu pusty pusty zestaw wyników. Tutaj możesz załadować swój token, ewentualnie odświeżyć go itp.

Częśćtrzecia część to to, co robisz, gdy masz wynik (lub błąd). Musisz wywołać odpowiedź dla każdego innego wątku, który mógł zostać zarejestrowany.

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

Wystarczy upewnić się, aby powrócić null na wszystkich ścieżkach podczas korzystania z response.

Oczywiście można użyć innych środków do synchronizowania tych bloków kodu, takich jak atomy, jak pokazano przez @ matrix w innej odpowiedzi. I korzystanie z synchronized, ponieważ uważam, że jest to najłatwiejszy do zrozumienia wdrożenia, ponieważ jest to wielka sprawa i każdy powinien to robić;)


Powyższy przykład jest dostosowana wersja o emitter loop described here, gdzie szczegółowo opisuje współbieżność. Ten blog jest świetnym źródłem, jeśli interesuje Cię, jak RxJava działa pod maską.

+0

Zabawa z tym i widzę ten FetchingToken nigdy nie jest prawdziwy. Każde wywołanie funkcji 'getAuthToken' działa przez całą metodę. Czy czegoś brakuje? – Jack

+0

@Jack Jeśli nie popełniłeś błędu podczas jego realizacji, oznacza to, że nie spotkałeś się z żadnymi warunkami wyścigowymi.Ten kod zapewnia, że ​​wiele wątków IFF potrzebuje nowego hasła AccessToken, który go zażąda i przekazuje wyniki innym. Mogłem to przetestować, unieważniając parametr accessToken przed wystrzeleniem 10+ wywołań API naraz w wątkach działających w tle. Następnie jeden wątek wykona wyszukiwanie, a pozostali poczekają na wynik. –

+0

Dzięki za skontaktowanie się z nami. Tak zakładałem. Nazywam 'getAuthToken()' z wewnątrz okhttp 'Authenticator'. Używam RxJava i wyrzucam wszystkie oryginalne żądania w wątku 'io', które właśnie kończy wywoływanie' getAuthToken' najwyraźniej nie w sposób, jakiego oczekiwałbym podczas synchronizacji w metodzie. Jestem całkiem nowy w synchronizacji, więc wciąż nad tym pracuję. Jeśli metoda zsynchronizowana jest wywoływana z tego samego wątku, czy nadal będzie czekać? – Jack