Подтвердить что ты не робот

Как использовать ReactiveCocoa для прозрачной аутентификации перед вызовом API?

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

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

// getThing returns RACSignal yielding the data returned by GET /thing.
// if the apiClient instance doesn't already have a token, it must
// retrieve one before calling GET /thing 
RAC(self.thing) = [apiClient getThing]; 

Как я могу использовать ReactiveCocoa, чтобы прозрачно вызывать первый (и только первый) запрос API для извлечения и, в качестве побочного эффекта, безопасно хранить маркер API перед любыми последующими запросами?

Также я хочу использовать combineLatest: (или подобное) для запуска нескольких одновременных запросов и что они все неявно ожидают получения токена.

RAC(self.tupleOfThisAndThat) = [RACSignal combineLatest:@[ [apiClient getThis], [apiClient getThat]]];

Кроме того, если запрос на получение-токен уже находится в полете при вызове API, этот вызов API должен дождаться завершения запроса на получение-токен.

Мое частичное решение следует:

Основной шаблон будет состоять в том, чтобы использовать flattenMap: для сопоставления сигнала, который дает токен сигналу, который, учитывая токен, выполняет требуемый запрос и дает результат вызова API.

Предполагая некоторые удобные расширения для NSURLRequest:

- (RACSignal *)requestSignalWithURLRequest:(NSURLRequest *)urlRequest {
    if ([urlRequest isSignedWithAToken])
        return [self performURLRequest:urlRequest];

    return [[self getToken] flattenMap:^ RACSignal * (id token) {
        NSURLRequest *signedRequest = [urlRequest signedRequestWithToken:token];
        assert([urlRequest isSignedWithAToken]);
        return [self requestSignalWithURLRequest:signedRequest];
    }
}

Теперь рассмотрим реализацию подписки -getToken.

  • В тривиальном случае, когда токен уже получен, подписка сразу же дает токен.
  • Если токен не был восстановлен, подписка отменяет вызов API аутентификации, который возвращает токен.
  • Если вызов API аутентификации находится в полете, безопасно добавлять другого наблюдателя, не вызывая повторения вызова API аутентификации по проводке.

Однако я не уверен, как это сделать. Кроме того, как и где безопасно хранить токен? Какой-то постоянный/повторяемый сигнал?

4b9b3361

Ответ 1

Итак, здесь есть две основные вещи:

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

Чтобы разделить побочные эффекты (# 1 выше), мы будем использовать RACMulticastConnection. Как говорится в документации:

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

Добавьте один из них в качестве частного свойства в клиентский класс API:

@interface APIClient ()
@property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
@end

Теперь это решит случай с N текущих подписчиков, которым все нужны одинаковые результаты в будущем (вызовы API ждут, когда токен запроса находится в полете), но нам все еще нужно что-то еще, чтобы гарантировать, что будущие подписчики получат одинаковый результат (уже выбранный токен), независимо от того, подписываются ли они.

Это то, что RACReplaySubject для:

Объект повтора сохраняет значения, которые он отправляет (до его определенной емкости), и передает их новым подписчикам. Он также воспроизведет ошибку или завершение.

Чтобы связать эти два понятия вместе, мы можем использовать RACSignal -multicast: method, который превращает обычный сигнал в соединение с помощью определенного вид предмета.

Мы можем подключить большую часть поведения во время инициализации:

- (id)init {
    self = [super init];
    if (self == nil) return nil;

    // Defer the invocation of -reallyGetToken until it actually needed.
    // The -defer: is only necessary if -reallyGetToken might kick off
    // a request immediately.
    RACSignal *deferredToken = [RACSignal defer:^{
        return [self reallyGetToken];
    }];

    // Create a connection which only kicks off -reallyGetToken when
    // -connect is invoked, shares the result with all subscribers, and
    // pushes all results to a replay subject (so new subscribers get the
    // retrieved value too).
    _tokenConnection = [deferredToken multicast:[RACReplaySubject subject]];

    return self;
}

Затем мы реализуем -getToken, чтобы вызвать выборку лениво:

- (RACSignal *)getToken {
    // Performs the actual fetch if it hasn't started yet.
    [self.tokenConnection connect];

    return self.tokenConnection.signal;
}

Впоследствии все, что подписывается на результат -getToken (например, -requestSignalWithURLRequest:), получит токен, если он еще не был выбран, при необходимости начните получать его или дождитесь запроса на рейс, если является одним.

Ответ 2

Как насчет

...

@property (nonatomic, strong) RACSignal *getToken;

...

- (id)init {
    self = [super init];
    if (self == nil) return nil;

    self.getToken = [[RACSignal defer:^{
        return [self reallyGetToken];
    }] replayLazily];
    return self;
}

Конечно, это решение функционально идентично ответу Джастина выше. В основном мы используем тот факт, что метод удобства уже существует в RACSignal public API:)

Ответ 3

Мышление о токене истечет позже, и мы должны его обновить.

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

Для первых нескольких запросов, поскольку там нет токена, сигнал запроса будет flatMap для ошибки и, таким образом, активирует refreshAT, пока мы не обновимToken, таким образом, будем запускать refreshRT и установить как at, так и rt на последнем этапе.

здесь полный код

static var headers = MutableProperty(["TICKET":""])
static let atLock = NSLock()
static let manager = Manager(
    configuration: NSURLSessionConfiguration.defaultSessionConfiguration()
)

internal static func GET(path:String!, params:[String: String]) -> SignalProducer<[String: AnyObject], NSError> {
    let reqSignal = SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        manager.request(Router.GET(path: path, params: params))
        .validate()
        .responseJSON({ (response) -> Void in
            if let error = response.result.error {
                sink.sendFailed(error)
            } else {
                sink.sendNext(response.result.value!)
                sink.sendCompleted()
            }
        })
    }

    return reqSignal.flatMapError { (error) -> SignalProducer<[String: AnyObject], NSError> in
            return HHHttp.refreshAT()
        }.flatMapError({ (error) -> SignalProducer<[String : AnyObject], NSError> in
            return HHHttp.refreshRT()
        }).then(reqSignal)
}

private static func refreshAT() -> SignalProducer<[String: AnyObject], NSError> {
    return SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        if atLock.tryLock() {
            Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
                .validate()
                .responseJSON({ (response) -> Void in
                    if let error = response.result.error {
                        sink.sendFailed(error)
                    } else {
                        let v = response.result.value!["data"]
                        headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
                        sink.sendCompleted()
                    }
                    atLock.unlock()
                })
        } else {
            headers.signal.observe(Observer(next: { value in
                print("get headers from local: \(value)")
                sink.sendCompleted()
            }))
        }
    }
}

private static func refreshRT() -> SignalProducer<[String: AnyObject], NSError> {
    return SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
        .responseJSON({ (response) -> Void in
            let v = response.result.value!["data"]                
            headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")                
            sink.sendCompleted()
        })
    }
}