iOS / Cocoa – NSURLSession – Manejo de la autorización HTTPS básica

[editado para proporcionar más información]

(No estoy usando AFNetworking para este proyecto. Puedo hacerlo en el futuro, pero primero deseo resolver este problema / malentendido).

CONFIGURACIÓN DEL SERVIDOR

No puedo proporcionar el service real aquí, pero es un service simple y confiable que devuelve XML de acuerdo a una URL como:

https: // nombre de usuario: contraseña@example.com/webservice

Quiero conectarme a la URL a través de HTTPS usando GET, y determinar cualquier falla de authentication (código de estado http 401).

He confirmado que el service web está disponible y que puedo con éxito (código de estado de http 200) tomar XML de la url usando un nombre de usuario y contraseña especificados. Lo he hecho con un browser web, y con AFNetworking 2.0.3, y con NSURLConnection.

También he confirmado que estoy usando las cnetworkingenciales correctas en todas las etapas.

Dado las cnetworkingenciales correctas y el siguiente código:

// Note: NO delegate provided here. self.sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:self.sessionConfig delegate:nil delegateQueue:nil]; NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:self.requestURL completionHandler: ... 

El código anterior funcionará . Se conectará correctamente al server, obtendrá un código de estado http de 200 y devolverá los datos (XML).

PROBLEMA 1

Este enfoque simple falla en los casos en que las cnetworkingenciales no son válidas. En ese caso, el bloque de finalización nunca se llama, no se proporciona ningún código de estado (401) y, eventualmente, el time de espera de la tarea.

INTENTÓ SOLUCIÓN

Asigné un delegado a NSURLSession, y estoy manejando las siguientes devoluciones de llamada:

 -(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCnetworkingential *cnetworkingential))completionHandler { if (_sessionFailureCount == 0) { NSURLCnetworkingential *cnetworking = [NSURLCnetworkingential cnetworkingentialWithUser:self.userName password:self.password persistence:NSURLCnetworkingentialPersistenceNone]; completionHandler(NSURLSessionAuthChallengeUseCnetworkingential, cnetworking); } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } _sessionFailureCount++; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCnetworkingential *cnetworkingential))completionHandler { if (_taskFailureCount == 0) { NSURLCnetworkingential *cnetworking = [NSURLCnetworkingential cnetworkingentialWithUser:self.userName password:self.password persistence:NSURLCnetworkingentialPersistenceNone]; completionHandler(NSURLSessionAuthChallengeUseCnetworkingential, cnetworking); } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } _taskFailureCount++; } 

PROBLEMA 1 CUANDO SE UTILIZA LA INTENTADA SOLUCIÓN

Tenga en count el uso de ivars _sessionFailureCount y _taskFailureCount. ¡Estoy usando esto porque la propiedad @previousFailureCount del object desafío nunca está avanzada! Siempre permanece en cero, sin importar cuántas veces se llamen a estos methods de callback.

PROBLEMA 2 CUANDO SE UTILIZA UNA SOLUCIÓN INTENTADA

A pesar del uso de cnetworkingenciales correctas (como lo demuestra su uso exitoso con un delegado nulo), la authentication está fallando.

Se producen las siguientes devoluciones de llamada:

 URLSession:didReceiveChallenge:completionHandler: (challenge @ previousFailureCount reports as zero) (_sessionFailureCount reports as zero) (completion handler is called with correct cnetworkingentials) (there is no challenge @error provided) (there is no challenge @failureResponse provided) URLSession:didReceiveChallenge:completionHandler: (challenge @ previousFailureCount reports as **zero**!!) (_sessionFailureCount reports as one) (completion handler is called with request to cancel challenge) (there is no challenge @error provided) (there is no challenge @failureResponse provided) // Finally, the Data Task's completion handler is then called on us. (the http status code is reported as zero) (the NSError is reported as NSURLErrorDomain Code=-999 "cancelled") 

(El NSError también proporciona una NSErrorFailingURLKey, que me muestra que la URL y las cnetworkingenciales son correctas).

Cualquier sugerencia bienvenida!

No necesita implementar un método delegado para esto, simplemente configure el encabezado HTTP de autorización en la request, p. Ej.

 NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://whatever.com"]]; NSString *authStr = @"username:password"; NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; NSString *authValue = [NSString stringWithFormat: @"Basic %@",[authData base64EncodedStringWithOptions:0]]; [request setValue:authValue forHTTPHeaderField:@"Authorization"]; //create the task NSURLSessionDataTask* task = [[NSURLSession shanetworkingSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { }]; 

Autenticación HTTP pregonada vs no anunciada

Me parece que toda la documentation sobre NSURLSession y la authentication HTTP omite el hecho de que el requisito de authentication se puede solicitar (como es el caso cuando se utiliza un file .htpassword) o no se ha enviado (como es el caso habitual cuando se trata de un service REST )

Para el caso solicitado, la estrategia correcta es implementar el método de delegado: URLSession:task:didReceiveChallenge:completionHandler: Para el caso no solicitado, la implementación del método de delegado solo le brindará la oportunidad de verificar el desafío de SSL (por ejemplo, el espacio de protección). Por lo tanto, cuando se trata de REST, es probable que deba agregar encabezados de Autenticación manualmente, como lo señaló @malhal.

Aquí hay una solución más detallada que omite la creación de una NSURLRequest.

  // // REST and unprompted HTTP Basic Authentication // // 1 - define cnetworkingentials as a string with format: // "username:password" // NSString *username = @"USERID"; NSString *password = @"SECRET"; NSString *authString = [NSString stringWithFormat:@"%@:%@", username, secret]; // 2 - convert authString to an NSData instance NSData *authData = [authString dataUsingEncoding:NSUTF8StringEncoding]; // 3 - build the header string with base64 encoded data NSString *authHeader = [NSString stringWithFormat: @"Basic %@", [authData base64EncodedStringWithOptions:0]]; // 4 - create an NSURLSessionConfiguration instance NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; // 5 - add custom headers, including the Authorization header [sessionConfig setHTTPAdditionalHeaders:@{ @"Accept": @"application/json", @"Authorization": authHeader } ]; // 6 - create an NSURLSession instance NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; // 7 - create an NSURLSessionDataTask instance NSString *urlString = @"https://API.DOMAIN.COM/v1/locations"; NSURL *url = [NSURL URLWithString:urlString]; NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler: ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { // do something with the error return; } NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode == 200) { // success: do something with returned data } else { // failure: do something else on failure NSLog(@"httpResponse code: %@", [NSString stringWithFormat:@"%ld", (unsigned long)httpResponse.statusCode]); NSLog(@"httpResponse head: %@", httpResponse.allHeaderFields); return; } }]; // 8 - resume the task [task resume]; 

Esperemos que esto ayude a cualquiera que se encuentre con esta diferencia mal documentada. Finalmente lo descubrí usando el código de testing, un proxy local ProxyApp y deshabilitando forzosamente NSAppTransportSecurity en el file Info.plist mi proyecto (necesario para inspeccionar el tráfico SSL a través de un proxy en iOS 9 / OSX 10.11).

Respuesta corta: el comportamiento que describe es consistente con un error de authentication de server básico. Sé que has informado de que has verificado que es correcto, pero sospecho que hay un problema de validation fundamental en el server (no tu código de iOS).

Respuesta larga:

  1. Si utiliza NSURLSession sin el delegado e incluye el ID de usuario / contraseña en la URL, entonces se NSURLSessionDataTask al completionHandler de NSURLSessionDataTask del NSURLSessionDataTask si la combinación ID de usuario / contraseña es correcta. Pero, si falla la authentication, NSURLSession parece intentar repetidamente realizar la request, utilizando las mismas cnetworkingenciales de authentication cada vez, y el completionHandler no parece recibir una llamada. (Me di count de que al ver la connection con Charles Proxy ).

    Esto no me parece muy prudente de NSURLSession , pero una vez más, la versión less delegada no puede hacer mucho más que eso. Al utilizar la authentication, usar el enfoque basado en delegate parece más sólido.

  2. Si utiliza NSURLSession con el delegate especificado (y ningún parámetro completionHandler al crear la tarea de datos), puede examinar la naturaleza del error en didReceiveChallenge , a saber, examinar los objects challenge.error y challenge.failureResponse . Es posible que desee actualizar su pregunta con esos resultados.

    Como un lado, parece que está manteniendo su propio contador _failureCount , pero probablemente pueda utilizar la propiedad challenge.previousFailureCount .

  3. Tal vez pueda compartir algunos detalles sobre la naturaleza de la authentication que está utilizando su server. Solo pregunto, porque cuando aseguro un directory en mi server web, no llama al método NSURLSessionDelegate :

     - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCnetworkingential *cnetworkingential))completionHandler 

    Pero más bien, llama al método NSURLSessionTaskDelegate :

     - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCnetworkingential *cnetworkingential))completionHandler 

Como dije, el comportamiento que describes consiste en una falla de authentication en el server. Compartir los detalles sobre la naturaleza de la configuration de authentication en su server y los detalles del object NSURLAuthenticationChallenge podrían ayudarnos a diagnosticar lo que está sucediendo. Es posible que también desee escribir la URL con el ID de usuario / contraseña en un browser web y que también pueda confirmar si existe un problema de authentication básica.