2012-12-28 24 views
25

我在調用遠程Web API的應用程序中使用ReactiveCocoa。但是,在從給定的API主機獲取任何東西之前,應用程序必須提供用戶的憑證並檢索API令牌,然後用它來簽署後續請求。如何使用ReactiveCocoa在進行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調用。

但是我不知道該怎麼做。另外,如何以及在哪裏安全地存儲令牌?某種持久/可重複的信號?

回答

45

因此,有這裏發生兩件重要的事情:

  1. 你想每次分享一些副作用(在這種情況下,獲取一個令牌),而無需重新觸發他們有一個新的用戶。
  2. 您希望任何訂閱-getToken的人都可以獲得相同的值,無論如何。

爲了分享副作用(上面#1),我們將使用RACMulticastConnection。如文檔所述:

多播連接將多個訂閱共享一個訂閱的想法封裝到多個訂戶。如果訂閱底層信號涉及副作用或者不應該多次調用,則這通常是需要的。

讓我們添加爲的API客戶端類的私人財產之一:

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

現在,這將解決N個當前用戶所有需要同未來結果的情況下(API調用等待在請求令牌正在進行中),但是我們仍然需要其他方法來確保訂閱者獲得相同的結果(已獲取的令牌),無論他們訂閱的時間是多少。

這是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:)如果尚未獲取該令牌,則會獲得該令牌,如果有必要,則開始獲取該令牌,或等待正在進行的請求(如果有)。

+0

真棒解釋。謝謝! – bvanderveen

+3

你會如何處理註銷?或多個帳戶? –

+0

@ColinBarrett每個人都需要自己的詳細答案 - 這只是上述問題的最簡單的解決方案。支持註銷可能涉及將tokenSignal放入容量爲1的RACReplaySubject中,以便您可以隨意向其中添加新的信號。多個賬戶將會是一個更大的變化,因爲想必API也需要更新。我很樂意在SO或GitHub上的一個新問題上更詳細地回答。 –

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中:)

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() 
     }) 
    } 
} 
相關問題