Обновить маркер доступа Retrofit2 + RXJava2 - PullRequest
0 голосов
/ 15 ноября 2018

Этот подход всегда работал при обновлении 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 Service

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, а со второго запроса все работает?

Ответы [ 2 ]

0 голосов
/ 15 ноября 2018

Я думаю, что нет необходимости использовать перехватчик, вместо этого вы реализуете 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();
}
0 голосов
/ 15 ноября 2018

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

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 получит новый доступтокен, и вы сохраняете его где-то, затем вы хотите использовать новый токен доступа для повторения запроса) вам нужно обернуть наблюдаемое с помощью оператора defer , чтобы принудительно создать новое наблюдаемое, как это:

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