Retrofit/Rxjava и сервисы на основе сеансов

Я реализую сервисы на основе сеансов. Все запросы должны быть подписаны с параметром сеанса cookie, который, в свою очередь, извлекается с помощью отдельного API-интерфейса. Таким образом, основной рабочий процесс будет состоять в том, чтобы получить файл cookie сеанса и продолжить выполнение запросов к службам. Иногда срок действия файла cookie истекал, и это приводило к другому запросу файла cookie сеанса.

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

Можете ли вы предложить идеи по реализации этого с Retrofit/RxJava? Я думаю, что SessionService должен быть инкапсулирован всеми другими службами, чтобы они могли запрашивать его всякий раз, когда это требуется, но я не уверен, как это сделать с RestAdapter.create Retrofit.


person midnight    schedule 28.08.2014    source источник


Ответы (1)


Я делал что-то подобное раньше, но с авторизацией OAuth. По сути, у вас есть RestAdapter, инициализированный с помощью RequestInterceptor, который добавляет файл cookie сеанса к каждому запросу. RequestInterceptor получает новый файл cookie сеанса каждый раз при авторизации сеанса.

В приведенном ниже примере кода используется следующее определение интерфейса Retrofit REST:

interface ApiService {
    @GET("/examples/v1/example")
    Observable<Example> getExample();
}

Перехватчик запросов просматривает каждый запрос REST и может добавлять заголовки, параметры запроса или изменять URL-адрес. В этом примере предполагается, что файл cookie добавляется в качестве заголовка HTTP.

class CookieHeaderProvider implements RequestInterceptor {
    private String sessionCookie = "";

    public CookieHeaderProvider() {
    }

    public void setSesstionCookie(String sessionCookie) {
        this.sessionCookie = sessionCookie;
    }

    @Override
    public void intercept(RequestFacade requestFacade) {
        requestFacade.addHeader("Set-Cookie", sessionCookie);
    }
}

Это SessionService, на который вы ссылались. Он отвечает за сетевой запрос, который авторизует/обновляет файл cookie сеанса.

class SessionService {
    // Modify contructor params to pass in anything needed 
    // to get the session cookie.
    SessionService(...) {
    }

    public Observable<String> observeSessionCookie(...) {
        // Modify to return an Observable that when subscribed to
        // will make the network request to get the session cookie.
        return Observable.just("Fake Session Cookie");
    }
}

Класс RestService обертывает интерфейс Retrofit, так что логика повторения запроса может быть добавлена ​​к каждому Retrofit Observable.

class RestService {
    private final apiService;
    private final sessionSerivce;
    private final cookieHeaderProvider;

    RestService(ApiService apiService, 
                SessionService sessionSerivce,
                CookieHeaderProvider cookieHeaderProvider) {
        this.apiService = apiService;
        this.sessionSerivce = sessionSerivce;
        this.cookieHeaderProvider = cookieHeaderProvider;
    }

    Observable<Example> observeExamples() {
        // Return a Retrofit Observable modified with
        // session retry logic.
        return 
            apiService
                .observeExamples()
                .retryWhen(new RetryWithSessionRefresh(sessionSerivce, cookieHeaderProvider)); 
    }
}

Приведенная ниже логика повторения будет использовать SessionService для обновления файла cookie сеанса, а затем повторить неудачные запросы REST, если файл cookie сеанса, отправленный на сервер, возвращает ошибку HTTP Unauthorized (401).

public class RetryWithSessionRefresh implements
        Func1<Observable<? extends Throwable>, Observable<?>> {

    private final SessionService sessionSerivce;
    private final CookieHeaderProvider cookieHeaderProvider;

    public RetryWithSessionRefresh(SessionService sessionSerivce,
                                   CookieHeaderProvider cookieHeaderProvider) {
        this.sessionSerivce = sessionSerivce;
        this.cookieHeaderProvider = cookieHeaderProvider;
    }

    @Override
    public Observable<?> call(Observable<? extends Throwable> attempts) {
        return attempts
                .flatMap(new Func1<Throwable, Observable<?>>() {
                    public int retryCount = 0;

                    @Override
                    public Observable<?> call(final Throwable throwable) {
                        // Modify retry conditions to suit your needs. The following
                        // will retry 1 time if the error returned was an
                        // HTTP Unauthoried (401) response.
                        retryCount++;
                        if (retryCount <= 1 && throwable instanceof RetrofitError) {
                            final RetrofitError retrofitError = (RetrofitError) throwable;
                            if (!retrofitError.isNetworkError()
                                    && retrofitError.getResponse().getStatus() == HttpStatus.SC_UNAUTHORIZED) {
                                return sessionSerivce
                                        .observeSessionCookie()
                                        .doOnNext(new Action1<String>() {
                                            @Override
                                            public void call(String sessionCookie) {
                                                // Update session cookie so that next
                                                // retrofit request will use it.
                                                cookieHeaderProvider.setSesstionCookie(sessionCookie);
                                            }
                                        })
                                        .doOnError(new Action1<Throwable>() {
                                            @Override
                                            public void call(Throwable throwable) {
                                                // Clear session cookie on error.
                                                cookieHeaderProvider.setSesstionCookie("");
                                            }
                                        });
                            }
                        }
                        // No more retries. Pass the original
                        // Retrofit error through.
                        return Observable.error(throwable);
                    }
                });
    }
}

Код инициализации клиента будет выглядеть примерно так:

CookieHeaderProvider cookieHeaderProvider = new CookieHeaderProvider();
SessionService sessionSerivce = new SessionService();

ApiService apiService =
    new RestAdapter.Builder()
        .setEndpoint(...)
        .setClient(...)
        .setRequestInterceptor(cookieHeaderProvider)
        .build()
        .create(ApiService.class);

RestService restService =
    new RestService(apiService, sessionSerivce, cookieHeaderProvider);

Затем получите наблюдаемый REST из RestService и подпишитесь на него, чтобы запустить сетевой запрос.

Observable<Example> exampleObservable =
    restService
        .observeExamples();

Subsctiption subscription =
    exampleObservable
        .subscribe(new Observer<Example>() {
            void onNext(Example example) {
                // Do stuff with example
            }
            void onCompleted() {
                // All done.
            }
            void onError(Throwalbe e) {
                // All API errors will end up here.
            }
        });
person kjones    schedule 28.08.2014
comment
На самом деле выглядит довольно аккуратно. Спасибо! - person midnight; 29.08.2014
comment
ошибка: несовместимые типы: RetryWithSessionRefresh нельзя преобразовать в Func1‹? супер наблюдаемый‹? расширяет Throwable›,? расширяет Observable‹?›› на самом деле работает только с подрывной версией RxJava от netflix - person desgraci; 25.06.2015
comment
@desgraci API retryWhen() был изменен в RxJava 1.0. Я обновил ответ для совместимости с 1.0+. - person kjones; 26.06.2015
comment
@kjones, я делал очень похожую реализацию раньше, и она отлично работает, я думаю, что ваше решение выглядит хорошо, так что у вас есть мой голос и мой топор (y) - person desgraci; 26.06.2015
comment
Престижность за этот ответ, он отлично работает. Мне интересно, можно ли объединить интерфейс OkHttp Authenticator с Rx. Я пробовал, и не все так просто или несовместимо... - person Nicolas Jafelle; 21.10.2016