Okhttp refresh expired token, когда несколько запросов отправляются на сервер

У меня есть ViewPager и три вызова webservice выполняются, когда ViewPager загружается одновременно.

Когда первый возвращает 401, вызывается Authenticator и я обновляю токен внутри Authenticator , но оставшиеся 2 запроса уже отправляются на сервер со старым токеном обновления и не удаются с 498, который зафиксирован в Interceptor, и приложение вышло из системы.

Это не идеальное поведение, которого я ожидал бы. Я хотел бы сохранить 2-й и 3-й запросы в очереди, а когда токен обновлен, повторите запрос в очереди.

В настоящее время у меня есть переменная, указывающая, продолжается ли обновление токенов в Authenticator , в этом случае я отменяю все последующие запросы в Interceptor и пользователь должен вручную обновить страницу, или я могу выйти из системы и заставить пользователя войти в систему.

Что такое хорошее решение или архитектура для вышеупомянутой проблемы с использованием okhttp 3.x для Android?

EDIT: Проблема, которую я хочу решить, в общем, и я не хотел бы упорядочивать мои звонки. Т.е. дождаться завершения одного вызова и обновить токен, а затем отправить остальную часть запроса на уровень активности и фрагмента.

Был запрошен код. Это стандартный код для 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 и Retrofit, обновить токен с одновременными запросами

Solutions Collecting From Web of "Okhttp refresh expired token, когда несколько запросов отправляются на сервер"

Важно отметить, что accountManager.blockingGetAuthToken (или неблокирующая версия) все равно можно было бы вызывать где-то в другом месте, кроме перехватчика. Следовательно, правильное место, чтобы предотвратить эту проблему, было бы в пределах аутентификатора.

Мы хотим убедиться, что первый поток, которому нужен токен доступа, будет извлекать его, а возможные другие потоки должны просто регистрироваться для вызова обратного вызова, когда первый поток завершит извлечение токена.
Хорошей новостью является то, что AbstractAccountAuthenticator уже имеет способ доставки асинхронных результатов, а именно AccountAuthenticatorResponse , на которые вы можете вызывать onResult или onError .


Следующий образец состоит из 3 блоков.

Первый заключается в том, чтобы убедиться, что только один поток извлекает токен доступа, а другие потоки только регистрируют свой response для обратного вызова.

Вторая часть – всего лишь пустая пустая совокупность результатов. Здесь вы загрузите свой токен, возможно, обновите его и т. Д.

Третья часть – это то, что вы делаете, когда у вас есть результат (или ошибка). Вы должны обязательно вызвать ответ для каждого другого потока, который мог быть зарегистрирован.

 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 работает под капотом.

Вы можете сделать это:

Добавьте их в качестве данных:

 // 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 (ie 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 запрос для обновления токена, а затем для каждого другого вы получите обновленный токен.

Вы можете попробовать с этим перехватчиком уровня приложения

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

Надеюсь это поможет!!!!