如何在进行API调用之前使用ReactiveCocoa进行透明身份验证?

时间:2012-12-28 08:20:14

标签: cocoa reactive-programming reactive-cocoa

我在一个应用程序中使用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的订阅实施。

  • 在简单的情况下,当已经检索到令牌时,订阅会立即生成令牌。
  • 如果尚未检索到令牌,则订阅将延迟到返回令牌的身份验证API调用。
  • 如果身份验证API调用正在进行中,则可以安全地添加另一个观察者,而不会导致通过网络重复进行身份验证API调用。

但是我不知道该怎么做。此外,如何以及在何处安全存储令牌?某种持久/可重复的信号?

3 个答案:

答案 0 :(得分:45)

所以,这里有两件大事:

  1. 您希望分享一些副作用(在这种情况下,获取令牌),而不是每次有新订阅者时重新触发它们。
  2. 您希望任何订阅-getToken的人都能获得相同的价值,无论如何。
  3. 为了分享副作用(上面的#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()
        })
    }
}