我在一个应用程序中使用ReactiveCocoa,该应用程序调用远程Web 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
的订阅实施。
但是我不知道该怎么做。此外,如何以及在何处安全存储令牌?某种持久/可重复的信号?
答案 0 :(得分:45)
所以,这里有两件大事:
-getToken
的人都能获得相同的价值,无论如何。为了分享副作用(上面的#1),我们将使用RACMulticastConnection。就像文档说的那样:
多播连接封装了向多个订户共享一个信号订阅的想法。如果对基础信号的订阅涉及副作用或不应多次调用,则通常需要这样做。
让我们在API客户端类中添加其中一个作为私有属性:
@interface APIClient ()
@property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
@end
现在,这将解决N个当前订户的情况,这些订户都需要相同的未来结果(等待请求令牌在飞行中的API调用),但我们还需要其他东西来确保 future 订阅者获得相同的结果(已经获取的令牌),无论他们何时订阅。
这是RACReplaySubject的用途:
重放主题保存发送的值(达到其定义的容量)并将其重新发送给新订户。它还将重播错误或完成。
为了将这两个概念结合在一起,我们可以使用RACSignal's -multicast: method,通过使用特定类型的主题将正常信号转换为连接。
我们可以在初始化时连接大多数行为:
- (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:
)结果的任何内容都会获得令牌(如果尚未获取),必要时开始获取,或者等待 - 如果有航班请求。
答案 1 :(得分: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;
}
可以肯定的是,这个解决方案与Justin上面的答案功能相同。基本上我们利用了RACSignal
的公共API中已存在便捷方法的事实:)
答案 2 :(得分: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()
})
}
}