iOS / Cocoa – NSURLSession – 处理基本的HTTPS授权

[编辑提供更多信息]

(我没有使用AFNetworking来做这个项目,我将来可能会这样做,但是希望先解决这个问题/误会。)

服务器设置

我不能在这里提供真正的服务,但它是一个简单,可靠的服务,根据以下URL返回XML:

https://开头的用户名:password@example.com/webservice

我想通过使用GET连接到通过HTTPS的URL,并确定任何身份validation失败(HTTP状态代码401)。

我已经确认Web服务可用,并且我可以成功(http状态码200)使用指定的用户名和密码从URL抓取XML。 我已经通过Web浏览器和AFNetworking 2.0.3完成了这个工作,并且使用了NSURLConnection。

我也证实,我在所有阶段使用正确的凭证。

鉴于正确的凭据和下面的代码:

// 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: ... 

上面的代码将工作 。 它将成功连接到服务器,获得一个200的http状态码,并返回(XML)数据。

问题1

这种简单的方法在证书无效的情况下失败。 在这种情况下,完成块从不被调用,不提供状态码(401),并且最终任务超时。

试图解决scheme

我将一个委托分配给NSURLSession,并处理以下callback:

 -(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { if (_sessionFailureCount == 0) { NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone]; completionHandler(NSURLSessionAuthChallengeUseCredential, cred); } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } _sessionFailureCount++; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { if (_taskFailureCount == 0) { NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone]; completionHandler(NSURLSessionAuthChallengeUseCredential, cred); } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } _taskFailureCount++; } 

问题1当使用试图解决scheme时

请注意使用ivars _sessionFailureCount和_taskFailureCount。 我正在使用这些,因为挑战对象的@previousFailureCount属性永远不会进步! 它始终保持为零,无论这些callback方法被调用多less次。

问题2当使用试图解决scheme时

尽pipe使用了正确的凭证(通过成功使用nil委托来certificate),authentication失败了。

以下callback发生:

 URLSession:didReceiveChallenge:completionHandler: (challenge @ previousFailureCount reports as zero) (_sessionFailureCount reports as zero) (completion handler is called with correct credentials) (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") 

(NSError还提供了一个NSErrorFailingURLKey,它显示了URL和凭证是正确的。)

任何build议欢迎!

您不需要为此实现委托方法,只需在请求上设置授权HTTP头,例如

 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 sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { }]; 

提示与无提示HTTP身份validation

在我看来,关于NSURLSession和HTTP身份validation的所有文档都会跳过这样一个事实,即可以提示身份validation的要求(如使用.htpassword文件的情况)或自发的 (如处理REST服务时的常见情况)。

对于提示的情况,正确的策略是实现委托方法: URLSession:task:didReceiveChallenge:completionHandler: 对于自发情况,委托方法的实施只会为您提供validationSSL挑战(例如保护空间)的机会。 因此,在处理REST时,您可能需要手动添加authentication标头,如@malhal指出的那样。

这是一个更详细的解决scheme,跳过创build一个NSURLRequest。

  // // REST and unprompted HTTP Basic Authentication // // 1 - define credentials 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]; 

希望这将有助于任何遇到这种logging差异的人。 我终于使用testing代码,一个本地代理ProxyApp,并在我的项目的Info.plist文件中强制禁用NSAppTransportSecurity (通过iOS 9 / OSX 10.11上的代理检查SSLstream量所必需的)来解决这个问题。

简答:您所描述的行为与基本的服务器身份validation失败一致。 我知道你已经报告你已经证实它是正确的,但我怀疑服务器上有一些根本的validation问题(不是你的iOS代码)。

很长的回答:

  1. 如果你没有委托使用NSURLSession并在URL中包含用户名/密码,那么如果用户名/密码组合正确,则NSURLSessionDataTask completionHandler块将被调用。 但是,如果身份validation失败, NSURLSession似乎会重复尝试发出请求,每次都使用相同的身份validation凭据,并且completionHandler似乎不会被调用。 (我注意到通过查看与Charles Proxy的连接)。

    这不会使我非常谨慎的NSURLSession ,但是再次无NSURLSession演绎实际上做不到这一点。 使用身份validation时,使用基于delegate的方法似乎更加健壮。

  2. 如果在创build数据任务时使用带有指定delegateNSURLSession (并且在创build数据任务时没有completionHandler参数),则可以检查NSURLSession错误的性质,即检查challenge.errorchallenge.failureResponse对象。 你可能想用这些结果来更新你的问题。

    _failureCount一下,你似乎正在维护自己的_failureCount计数器,但是你可以利用challenge.previousFailureCount属性。

  3. 也许你可以分享一些有关你的服务器使用的authentication性质的细节。 我只问,因为当我在我的Web服务器上保护目录时,它不调用NSURLSessionDelegate方法:

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

    但是,它调用NSURLSessionTaskDelegate方法:

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

就像我所说的,你描述的行为是由服务器上的authentication失败组成的。 在服务器上共享有关身份validation设置性质的细节以及NSURLAuthenticationChallenge对象的详细信息可能有助于我们诊断发生了什么事情。 您可能还想要在Web浏览器中input带有用户名/密码的URL,并且还可以确认是否存在基本身份validation问题。