25

リモートWebAPIを呼び出すアプリでReactiveCocoaを使用しています。ただし、特定の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呼び出しが行われたときに、retrieve-token要求がすでに実行中である場合、そのAPI呼び出しは、retrieve-token要求が完了するまで待機する必要があります。

私の部分的な解決策は次のとおりです。

基本的なパターンは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呼び出しをネットワーク上で繰り返さずに、別のオブザーバーを追加しても安全です。

ただし、これを行う方法がわかりません。また、トークンを安全に保管する方法と場所は?ある種の永続的/反復可能な信号?

4

3 に答える 3

45

したがって、ここで起こっている2つの主要なことがあります。

  1. 新しいサブスクライバーが存在するたびに副作用を再トリガーせずに、いくつかの副作用(この場合はトークンのフェッチ)を共有したいとします。
  2. 購読している人-getTokenには、何があっても同じ値を取得してもらいたいと考えています。

副作用(上記の#1)を共有するために、RACMulticastConnectionを使用します。ドキュメントが言うように:

マルチキャスト接続は、1つのサブスクリプションを多くのサブスクライバーへの信号に共有するという考えをカプセル化します。これは、基になるシグナルへのサブスクリプションに副作用が含まれる場合、または複数回呼び出されるべきでない場合に最も頻繁に必要になります。

それらの1つをAPIクライアントクラスのプライベートプロパティとして追加しましょう。

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

これで、現在のサブスクライバーN人がすべて同じ将来の結果を必要とする場合(API呼び出しが処理中のリクエストトークンを待機している)を解決できますが、将来のサブスクライバーが同じ結果を確実に取得できるようにするために、他に何かが必要です(すでに-フェッチされたトークン)、サブスクライブするタイミングに関係なく。

これがRACReplaySubjectの目的です。

リプレイサブジェクトは、送信された値を(定義された容量まで)保存し、それらを新しいサブスクライバーに再送信します。また、エラーまたは完了を再生します。

これら2つの概念を結び付けるために、RACSignalの-multicast:メソッドを使用できます。このメソッドは、特定の種類のサブジェクトを使用して、通常の信号を接続に変換します。

初期化時にほとんどの動作をフックできます。

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

    // Defer the invocation of -reallyGetToken until it's 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:)の結果をサブスクライブするものは、トークンがまだフェッチされていない場合はトークンを取得し、必要に応じてフェッチを開始するか、実行中のリクエストがある場合はそれを待ちます。

于 2012-12-28T16:18:41.977 に答える
3

どうですか

...

@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基本的に、コンビニエンスメソッドがのパブリックAPIにすでに存在するという事実を利用します:)

于 2013-10-20T07:11:42.813 に答える
0

トークンについて考えると、後で期限切れになり、更新する必要があります。

トークンをMutablePropertyに保存し、ロックを使用して、トークンを更新するための複数の期限切れの要求を防ぎます。トークンが取得または更新されたら、新しいトークンで再度要求します。

最初のいくつかのリクエストでは、トークンがないため、リクエストシグナルはflatMapでエラーになり、refreshATをトリガーしますが、refreshTokenがないため、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()
        })
    }
}
于 2016-10-19T03:34:36.103 に答える