Обновить токен доступа Retrofit2 + RXJava2

Этот подход всегда работал при обновлении token. То есть при каждом запросе, если я получил error 401, оператор retryWhen() запускал его, обновляя токен.

Вот код:

private Observable<TokenModel> refreshAccessToken() {
    Map<String, String> requestBody = new HashMap<>();
    requestBody.put(Constants.EMAIL_KEY, Constants.API_EMAIL);
    requestBody.put(Constants.PASSWORD_KEY, Constants.API_PASSWORD);

    return RetrofitHelper.getApiService().getAccessToken(requestBody)
            .subscribeOn(Schedulers.io())
            .doOnNext((AccessToken refreshedToken) -> {
                PreferencesHelper.putAccessToken(mContext, refreshedToken);
            });
}

public Function<Observable<Throwable>, ObservableSource<?>> isUnauthorized (){
    return throwableObservable -> throwableObservable.flatMap((Function<Throwable, ObservableSource<?>>) (Throwable throwable) -> {
        if (throwable instanceof HttpException) {
            HttpException httpException = (HttpException) throwable;

            if (httpException.code() == 401) {
                return refreshAccessToken();
            }
        }
        return Observable.error(throwable);
    });
}

Звоню isUnauthorized() оператору retryWhen(), где делаю запрос к серверу

class RetrofitHelper {

    static ApiService getApiService() {
        return initApi();
    }

    private static OkHttpClient createOkHttpClient() {
        final OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(chain -> {
            Request originalRequest = chain.request();

            AccessToken accessToken= PreferencesHelper.getAccessToken(BaseApplication.getInstance());
            String accessTokenStr = accessToken.getAccessToken();
            Request.Builder builder =
                    originalRequest.newBuilder().header("Authorization", "Bearer " + accessTokenStr);

            Request newRequest = builder.build();
            return chain.proceed(newRequest);
        });

        return httpClient.build();
    }

    private static ApiService initApi(){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Constants._api_url)
                .addConverterFactory(GsonConverterFactory.create())
                .addConverterFactory(ScalarsConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(createOkHttpClient())
                .build();
        return retrofit.create(ApiService.class);
    }
}

Но недавно мы добавили Basic Auth, и теперь по первому запросу я получаю 401, а retryWhen() пытается обновить токен, но все равно получает 401. То есть doOnNext() не работает, а сразу onError() работает

private static Observable<AccessToken> refreshAccessToken() {
    return RetrofitHelper.getApiService()
            .getAccessToken(
                    Credentials.basic(
                            Constants._API_USERNAME, Constants._API_PASSWORD
                    ),
                    Constants._API_BODY_USERNAME,
                    Constants._API_BODY_PASSWORD,
                    Constants._API_BODY_GRANT_TYPE
            )
            .doOnNext((AccessToken refreshedToken) -> {
                PreferencesHelper.putObject(BaseApplication.getInstance(), PreferenceKey.ACCESS_TOKEN_KEY, refreshedToken);
                }

            });
}

// Служба API

public interface ApiService {
    // Get Bearer Token
    @FormUrlEncoded
    @POST("oauth/token")
    Observable<AccessToken> getAccessToken(@Header("Authorization") String basicAuth,
                                           @Field("username") String username,
                                           @Field("password") String password,
                                           @Field("grant_type") String grantType);
}

Вот скажите, почему это ошибка? Почему при первом запросе у меня получается 401, а при втором все работает?

если вы используете retrofit2, вам нужно удалить / из конца вашего базового URL и добавить его перед своим сообщением @POST("/oauth/token")

karan 15.11.2018 13:58

@KaranMer, а почему?

No Name 15.11.2018 14:19
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
2
661
2

Ответы 2

Я хочу предложить лучшее решение.

public class RefreshTokenTransformer<T extends Response<?>> implements ObservableTransformer<T, T> {

    private class HttpCode {
        private static final int UNAUTHORIZED_HTTP_CODE = 401;
    }

    private ApiService mApiService;
    private UserRepository mUserRepository;

    public RefreshTokenTransformer(ApiService service, UserRepository userRepository) {
        mApiService = service;
        mUserRepository = userRepository;
    }

    @Override
    public ObservableSource<T> apply(final Observable<T> stream) {
        return stream.flatMap(new Function<T, ObservableSource<T>>() {
            @Override
            public ObservableSource<T> apply(T response) throws Exception {
                if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
                    return mApiService.refreshToken(mUserRepository.getRefreshTokenHeaders())
                            .filter(new UnauthorizedPredicate<>(mUserRepository))
                            .flatMap(new Function<Response<TokenInfo>, ObservableSource<T>>() {
                                @Override
                                public ObservableSource<T> apply(Response<TokenInfo> tokenResponse) throws Exception {
                                    return stream.filter(new UnauthorizedPredicate<T>(mUserRepository));
                                }
                            });
                }

                return stream;
            }
        });
    }

    private class UnauthorizedPredicate<R extends Response<?>> implements Predicate<R> {

        private UserRepository mUserRepository;

        private UnauthorizedPredicate(UserRepository userRepository) {
            mUserRepository = userRepository;
        }

        @Override
        public boolean test(R response) throws Exception {
            if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
                throw new SessionExpiredException();
            }

            if (response.body() == null) {
                throw new HttpException(response);
            }

            Class<?> responseBodyClass = response.body().getClass();
            if (responseBodyClass.isAssignableFrom(TokenInfo.class)) {
                try {
                    mUserRepository.validateUserAccess((TokenInfo) response.body());
                } catch (UnverifiedAccessException error) {
                    throw new SessionExpiredException(error);
                }
            }

            return true;
        }
    }
}

Я написал собственный оператор, который выполняет следующие действия:

  1. первый запрос запущен, и мы получаем код ответа 401;

  2. затем выполняем запрос / refresh_token для обновления токена;

  3. после этого, если токен успешно обновился, мы повторяем первый запрос. если токен / refresh_token не работает, мы генерируем исключение

Затем вы можете легко реализовать его в любом таком запросе:

 Observable
    .compose(new RefreshTokenResponseTransformer<Response<{$your_expected_result}>>
(mApiService, mUserRepository()));

Еще один важный момент: Скорее всего, у вашей начальной наблюдаемой для модернизации есть такие параметры:

mApiService.someRequest(token)

если ожидается, что параметр изменится во время выполнения RefreshTokenTransformer (например, запрос / refresh_token получит новый токен доступа, и вы его где-то сохраните, тогда вы хотите использовать новый токен доступа для повторения запроса), вам нужно будет обернуть наблюдаемое с помощью отложить оператор для принудительного создания нового наблюдаемого, например:

Observable.defer(new Callable<ObservableSource<Response<? extends $your_expected_result>>>() {
            @Override
            public Response<? extends $your_expected_result> call() throws Exception {
                return mApiService.someRequest(token);
            }
        })

Спасибо за развернутый ответ. Что это за классы: SessionExpiredException, UserRepository, UnverifiedAccessException?

No Name 15.11.2018 15:09

Это мои собственные классы наследования. SessionExpiredException и UnverifiedAccessException - расширяется от исключения для лучшей обработки ошибок. UserRepository - это также мой собственный класс, который инкапсулирует некоторую логику с хранением пользовательской информации. Собственно, это не часть идеи преобразователя обновления токенов, а всего лишь часть моей собственной логики.

Onix 15.11.2018 16:01

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

  public class TokenAuthenticator implements Authenticator {
@Override
  public Request authenticate(Proxy proxy,     Response response) throws IOException {
      // Refresh your access_token using a    synchronous api request
    newAccessToken = service.refreshToken();

          // Add new header to rejected request and retry   it
          return response.request().newBuilder()
            .header(AUTHORIZATION,    newAccessToken)
            .build();
}

Вы можете мне больше рассказать?

No Name 15.11.2018 14:21

Причина, по которой вы можете получить 401 при первом запросе, потому что ваш первый запрос должен быть запросом на вход и получить токен, и с этим токеном вы можете сделать свой запрос, и в случае, если срок действия вашего токена истек, будет вызван токен обновления.

Amir 16.11.2018 10:25

Другие вопросы по теме