reactiveswift - ¿Cómo utilizar ReactiveCocoa para autenticar de forma transparente antes de hacer llamadas a la API?
reactivecocoa swift (3)
Estoy usando ReactiveCocoa en una aplicación que realiza llamadas a API web remotas. Pero antes de poder recuperar cualquier cosa de un host de API dado, la aplicación debe proporcionar las credenciales del usuario y recuperar un token de API, que luego se utiliza para firmar solicitudes posteriores.
Quiero abstraer este proceso de autenticación para que ocurra automáticamente cada vez que hago una llamada a la API. Supongamos que tengo una clase de cliente API que contiene las credenciales del usuario.
// 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];
¿Cómo puedo usar ReactiveCocoa para generar de manera transparente la primera solicitud (y solo la primera) a una API para recuperar y, como efecto secundario, almacenar de forma segura un token de API antes de realizar cualquier solicitud posterior?
También es un requisito que pueda usar combineLatest:
(o similar) para iniciar varias solicitudes simultáneas y que todas esperarán implícitamente a que se recupere el token.
RAC(self.tupleOfThisAndThat) = [RACSignal combineLatest:@[ [apiClient getThis], [apiClient getThat]]];
Además, si la solicitud de recuperación de token ya está en vuelo cuando se realiza una llamada de API, esa llamada de API debe esperar hasta que la solicitud de recuperación de token se haya completado.
Mi solución parcial sigue:
El patrón básico será utilizar flattenMap:
para asignar una señal que ceda el token a una señal que, dado el token, realice la solicitud deseada y produzca el resultado de la llamada a la API.
Suponiendo algunas extensiones convenientes para 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];
}
}
Ahora considere la implementación de suscripción de -getToken
.
- En el caso trivial, cuando el token ya se ha recuperado, la suscripción produce el token inmediatamente.
- Si el token no se ha recuperado, la suscripción se transfiere a una llamada a la API de autenticación que devuelve el token.
- Si la llamada a la API de autenticación está en vuelo, debería ser seguro agregar otro observador sin hacer que la llamada a la API de autenticación se repita por el cable.
Sin embargo no estoy seguro de cómo hacer esto. Además, ¿cómo y dónde guardar el token de forma segura? ¿Algún tipo de señal persistente / repetible?
El pensamiento sobre el token expirará más tarde y tenemos que actualizarlo.
Almaceno el token en un MutableProperty, y utilicé un bloqueo para evitar que varias solicitudes caducadas actualizaran el token, una vez que se gana o se actualiza el token, solo solicite nuevamente con el nuevo token.
Para las primeras solicitudes, ya que no hay token, la señal de solicitud se convertirá en flatMap a error y, por lo tanto, activará refreshAT, mientras tanto no tenemos refreshToken, por lo tanto activamos refreshRT, y configuramos tanto a como rt en el paso final.
aquí está el código completo
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()
})
}
}
Entonces, hay dos cosas principales que están sucediendo aquí:
- Desea compartir algunos efectos secundarios (en este caso, obtener un token) sin tener que volver a activarlos cada vez que haya un nuevo suscriptor.
- Usted quiere que cualquiera que se suscriba a
-getToken
obtenga los mismos valores sin importar qué.
Para compartir efectos secundarios (# 1 arriba), usaremos RACMulticastConnection . Como dice la documentación:
Una conexión de multidifusión encapsula la idea de compartir una suscripción a una señal para muchos suscriptores. La mayoría de las veces, esto es necesario si la suscripción a la señal subyacente involucra efectos secundarios o no debe llamarse más de una vez.
Agreguemos uno de esos como una propiedad privada en la clase de cliente API:
@interface APIClient ()
@property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
@end
Ahora, esto resolverá el caso de N suscriptores actuales que todos necesitan el mismo resultado futuro (las llamadas a la API en espera de que el token de solicitud esté en vuelo), pero todavía necesitamos algo más para asegurar que los futuros suscriptores obtengan el mismo resultado (el ya existente). (token tofched), no importa cuando se suscriben.
Esto es para lo que RACReplaySubject es:
Un sujeto de repetición guarda los valores que se envía (hasta su capacidad definida) y los reenvía a nuevos suscriptores. También se repetirá un error o finalización.
Para unir estos dos conceptos, podemos usar el método -multicast: RACSignal , que convierte una señal normal en una conexión mediante el uso de un tipo específico de sujeto.
Podemos conectar la mayoría de los comportamientos en el momento de la inicialización:
- (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;
}
Luego, implementamos -getToken
para desencadenar la búsqueda perezosamente:
- (RACSignal *)getToken {
// Performs the actual fetch if it hasn''t started yet.
[self.tokenConnection connect];
return self.tokenConnection.signal;
}
Luego, cualquier cosa que se suscriba al resultado de -getToken
(como -getToken
obtendrá el token si aún no se ha recuperado, comience a buscarlo si es necesario, o espere una solicitud de vuelo si la hay.
Qué tal si
...
@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;
}
Para estar seguros, esta solución es funcional idéntica a la respuesta de Justin anterior. Básicamente, aprovechamos el hecho de que el método de conveniencia ya existe en la API pública de RACSignal
:)