【问题标题】:iOS/Cocoa - NSURLSession - Handling Basic HTTPS AuthorizationiOS/Cocoa - NSURLSession - 处理基本 HTTPS 授权
【发布时间】:2014-01-14 03:00:05
【问题描述】:

[编辑以提供更多信息]

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

服务器设置

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

https://username:password@example.com/webservice

我想使用 GET 通过 HTTPS 连接到 URL,并确定任何身份验证失败(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: ...

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

问题 1

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

尝试的解决方案

我为 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++;
}

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

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

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

尽管使用了正确的凭据(通过它们与 nil 委托的成功使用证明),但身份验证失败。

发生以下回调:

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 和凭据是正确的。)

欢迎提出任何建议!

【问题讨论】:

  • 请分享NSURLAuthenticationChallenge对象的详细信息。你说 challenge.errorchallenge.failureResponse 没有提供(我觉得很奇怪)。 challenge.protectionSpace 怎么样?还是说challenge 本身就是nil

标签: ios cocoa http authentication nsurlsession


【解决方案1】:

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

 }];

【讨论】:

  • 这对我有用,但请注意类参考说:“NSURLConnection 类和 NSURLSession 类旨在为您处理 HTTP 协议的各个方面。因此,您不应该修改以下标题:Authorization、Connection、Host、WWW-Authenticate"
  • @JosephLord 当你知道给定的请求将返回 403 而你只是浪费用户的电池时,当他们设计一个需要 403 响应才能指定凭据的糟糕界面时,你不应该关心苹果说什么和这种糟糕设计的带宽。
  • @CarlLee 如果您违反文档,则以后的更改会导致问题的风险更大,因此值得关注,尽管有时(在这种情况下似乎)仍然是正确的做法。应该有适当的 API(或更改文档以支持此用途)来预先提供授权信息。
  • @CarlLee 设计是否糟糕也没关系,Apple 会期望您按预期使用它的框架,如果您以不同的方式做事,框架中的更改会破坏您的代码。更不用说你的程序员同事会期望你遵循文档,所以真的没有任何理由开始自己发明轮子......
  • @CarlLee 是 401,而不是 403,并且该行为是由 HTTP 标准指定的,而不是 Apple 指定的。
【解决方案2】:

提示与非提示 HTTP 身份验证

在我看来,所有关于 NSURLSession 和 HTTP 身份验证的文档都忽略了这样一个事实,即身份验证要求可以是提示(就像使用 .htpassword 文件时的情况一样)或 自发(在处理 REST 服务时通常是这种情况)。

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

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

希望这对遇到这种记录不充分的差异的人有所帮助。我终于使用测试代码、本地代理 ProxyApp 并在我的项目的 Info.plist 文件中强制禁用 NSAppTransportSecurity (通过 iOS 9/OSX 10.11 上的代理检查 SSL 流量所必需的)解决了这个问题。

【讨论】:

    【解决方案3】:

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

    长答案:

    1. 如果您在没有代理的情况下使用 NSURLSession 并在 URL 中包含用户 ID/密码,则如果用户 ID/密码组合正确,则将调用 NSURLSessionDataTaskcompletionHandler 块。但是,如果身份验证失败,NSURLSession 似乎会重复尝试发出请求,每次都使用相同的身份验证凭据,而completionHandler 似乎不会被调用。 (通过观看与Charles Proxy 的连接,我注意到了这一点)。

      这并没有让我觉得NSURLSession 非常谨慎,但是再一次,无代表的演绎真的不能做更多的事情。使用身份验证时,使用基于delegate 的方法似乎更健壮。

    2. 如果您使用NSURLSession 并指定delegate(并且在创建数据任务时没有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
      

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

    【讨论】:

    • 感谢您对 Rob 的帮助。非常感激!我已经编辑了我的问题以提供更多信息。尤其是挑战的“previousFailureCount”属性缺乏进步令人担忧。
    • @Rob:出于好奇,当您提到保护服务器上的目录时,这是使用简单的基本身份验证.htaccess 文件吗?我这样做了,我似乎永远无法获得NSURLSessionDelegateNSURLSessionTaskDelegatedidReceiveChallenge 回调。相反,我通过为NSURLSessionConfiguration 对象提供HTTPAdditionalHeaders 来处理身份验证。如果我不这样做,我会在标头响应中得到状态代码 404。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-07
    相关资源
    最近更新 更多