25

我在调用远程 Web API 的应用程序中使用 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 调用时检索令牌请求已经在进行中,则该 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 调用通过网络重复。

但是我不确定如何做到这一点。另外,如何以及在哪里安全地存储令牌?某种持久/可重复的信号?

4

3 回答 3

45

因此,这里发生了两件事:

  1. 您想分享一些副作用(在这种情况下,获取令牌),而无需在每次有新订阅者时重新触发它们。
  2. -getToken无论如何,您希望任何订阅的人都能获得相同的值。

为了分享副作用(上面的#1),我们将使用RACMulticastConnection。就像文档说的那样:

多播连接封装了将一个订阅共享给多个订阅者的想法。如果对基础信号的订阅涉及副作用或不应多次调用,则通常需要这样做。

让我们将其中一个添加为 API 客户端类的私有属性:

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

现在,这将解决 N 个当前订阅者都需要相同的未来结果的情况(API 调用等待请求令牌正在进行中),但我们仍然需要其他东西来确保未来的订阅者获得相同的结果(已经-fetched token),无论他们何时订阅。

这就是RACReplaySubject的用途:

重播主题保存它发送的值(直到其定义的容量)并将这些值重新发送给新的订阅者。它还将重播错误或完成。

要将这两个概念联系在一起,我们可以使用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(like -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 中,并使用锁来防止多个过期请求刷新令牌,一旦获得或刷新令牌,只需使用新令牌再次请求。

对于前几个请求,由于没有token,请求信号会flatMap到error,从而触发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 回答