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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21105322/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:07:08  来源:igfitidea点击:

iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization

ioscocoahttpauthenticationnsurlsession

提问by Womble

[edited to provide more information]

[编辑以提供更多信息]

(I'm not using AFNetworking for this project. I may do so in future, but wish to resolve this problem/misunderstanding first.)

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

SERVER SETUP

服务器设置

I cannot provide the real service here, but it is a simple, reliable service that returns XML according to a URL such as:

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

https://username:[email protected]/webservice

https://username:[email protected]/webservice

I want to connect to the URL over HTTPS using GET, and determine any authentication failures (http status code 401).

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

I have confirmed that the web service is available, and that I can successfully (http status code 200) grab XML from the url using a specified username and password. I have done this with a web browser, and with AFNetworking 2.0.3, and by using NSURLConnection.

我已确认 Web 服务可用,并且我可以使用指定的用户名和密码成功(http 状态代码 200)从 url 中获取 XML。我已经使用 Web 浏览器和 AFNetworking 2.0.3 并使用 NSURLConnection 完成了此操作。

I have also confirmed that I am using the correct credentials at all stages.

我还确认我在所有阶段都使用了正确的凭据。

Given the correct credentials and the the following code:

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

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

The above code will work. It will successfully connect to the server, get a http status code of 200, and return the (XML) data.

上面的代码会起作用。它将成功连接到服务器,获取 http 状态码 200,并返回 (XML) 数据。

PROBLEM 1

问题1

This simple approach fails in cases where the credentials are invalid. In that case, the completion block is never called, no status code (401) is provided, and eventually, the Task times out.

在凭据无效的情况下,这种简单的方法会失败。在这种情况下,永远不会调用完成块,也不会提供状态代码 (401),最终任务超时。

ATTEMPTED SOLUTION

尝试的解决方案

I assigned a delegate to the NSURLSession, and am handling the following callbacks:

我为 NSURLSession 分配了一个委托,并正在处理以下回调:

-(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++;
}

PROBLEM 1 WHEN USING ATTEMPTED SOLUTION

使用尝试解决方案时的问题 1

Please note the use of ivars _sessionFailureCount and _taskFailureCount. I am using these because the challenge object's @previousFailureCount property is never advanced! It always remains at zero, no matter how many times these callback methods are called.

请注意 ivars _sessionFailureCount 和 _taskFailureCount 的使用。我使用这些是因为挑战对象的 @previousFailureCount 属性从未升级过!无论这些回调方法被调用多少次,它始终保持为零。

PROBLEM 2 WHEN USING ATTEMPTED SOLUTION

使用尝试解决方案时的问题 2

Despite the use of correct credentials (as proven by their successful use with a nil delegate), authentication is failing.

尽管使用了正确的凭据(正如它们与 nil 委托的成功使用所证明的那样),身份验证还是失败了。

The following callbacks occur:

发生以下回调:

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")

(The NSError also provides a NSErrorFailingURLKey, which shows me that the URL and credentials are correct.)

(NSError 还提供了一个 NSErrorFailingURLKey,它告诉我 URL 和凭据是正确的。)

Any suggestions welcome!

欢迎任何建议!

回答by malhal

You don't need to implement a delegate method for this, simply set the authorization HTTP header on the request, e.g.

您不需要为此实现委托方法,只需在请求上设置授权 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) {

 }];

回答by markeissler

Prompted vs Unprompted HTTP Authentication

提示与非提示 HTTP 身份验证

It seems to me that all documentation on NSURLSession and HTTP Authentication skips over the fact that the requirement for authentication can be prompted(as is the case when using an .htpassword file) or unprompted(as is the usual case when dealing with a REST service).

在我看来,关于NSURLSession和HTTP认证跳过所有文件在事实认证的要求可以提示(如使用.htpassword文件时的情况)或自发的(如REST服务打交道时是通常的情况下, )。

For the prompted case, the correct strategy is to implement the delegate method: URLSession:task:didReceiveChallenge:completionHandler:; for the unprompted case, implementation of the delegate method will only provide you with the opportunity to verify the SSL challenge (e.g. the protection space). Therefore, when dealing with REST, you will likely need to add Authentication headers manually as @malhal pointed out.

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

Here is a more detailed solution that skips the creation of an NSURLRequest.

这是一个更详细的解决方案,它跳过了 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];

Hopefully this will help anyone that runs into this poorly documented difference. I finally figured it out using test code, a local proxy ProxyAppand forcibly disabling NSAppTransportSecurityin my project's Info.plistfile (necessary for inspecting SSL traffic via a proxy on iOS 9/OSX 10.11).

希望这会帮助任何遇到这种记录不佳的差异的人。我最终使用测试代码、本地代理ProxyAppNSAppTransportSecurity在我的项目Info.plist文件中强行禁用(通过 iOS 9/OSX 10.11 上的代理检查 SSL 流量所必需的)。

回答by Rob

Short answer: The behavior you describe is consistent with a basic server authentication failure. I know you've reported that you've verified that it's correct, but I suspect some fundamental validation problem on the server (not your iOS code).

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

Long answer:

长答案:

  1. If you use NSURLSessionwithout the delegate and include the userid/password in the URL, then completionHandlerblock of the NSURLSessionDataTaskwill be called if the userid/password combination is correct. But, if the authentication fails, NSURLSessionappears to repeatedly attempt to make the request, using the same authentication credentials every time, and the completionHandlerdoesn't appear to get called. (I noticed that by watching the connection with Charles Proxy).

    This doesn't strike me as very prudent of NSURLSession, but then again the delegate-less rendition can't really do much more than that. When using authentication, using the delegate-based approach seems more robust.

  2. If you use the NSURLSessionwith the delegatespecified (and no completionHandlerparameter when you create the data task), you can examine the nature of the error in didReceiveChallenge, namely examine the challenge.errorand the challenge.failureResponseobjects. You might want to update your question with those results.

    As an aside, you appear to be maintaining your own _failureCountcounter, but you can probably avail yourself of challenge.previousFailureCountproperty, instead.

  3. Perhaps you can share some particulars about the nature of the authentication your server is using. I only ask, because when I secure a directory on my web server, it does not call the NSURLSessionDelegatemethod:

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

    But rather, it calls the NSURLSessionTaskDelegatemethod:

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                                didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
                                  completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
    
  1. 如果您在NSURLSession没有委托的情况下使用并在 URL 中包含用户 ID/密码,则在用户 ID/密码组合正确completionHandler的情况下NSURLSessionDataTask将调用块。但是,如果身份验证失败,NSURLSession似乎会反复尝试发出请求,每次都使用相同的身份验证凭据,并且completionHandler似乎不会被调用。(我通过观察与Charles Proxy的连接注意到了这一点)。

    这并没有让我NSURLSession对 . 使用身份验证时,使用delegate-based 方法似乎更健壮。

  2. 如果您使用NSURLSessiondelegate指定的(无completionHandler参数当您创建数据任务),您可以检查在错误的性质didReceiveChallenge,即检查challenge.errorchallenge.failureResponse对象。您可能想用这些结果更新您的问题。

    顺便说一句,您似乎在维护自己的_failureCount柜台,但您可能可以利用自己的challenge.previousFailureCount财产。

  3. 也许您可以分享一些有关您的服务器正在使用的身份验证性质的细节。我只是问,因为当我保护 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
    

Like I said, the behavior you describe is consist with an authentication failure on the server. Sharing the details about the nature of the authentication setting on your server and the particulars of the NSURLAuthenticationChallengeobject might help us diagnose what's going on. You might also want to type the URL with the userid/password in a web browser and that might also confirm whether there is a basic authentication problem.

就像我说的,您描述的行为与服务器上的身份验证失败有关。共享有关服务器上身份验证设置的性质的详细信息以及NSURLAuthenticationChallenge对象的详细信息可能有助于我们诊断正在发生的事情。您可能还想在 Web 浏览器中键入带有用户 ID/密码的 URL,这也可能确认是否存在基本身份验证问题。