Подтвердить что ты не робот

IOS/Cocoa - NSURLSession - обработка базовой авторизации HTTPS

[отредактировано, чтобы предоставить дополнительную информацию]

(Я не использую AFNetworking для этого проекта. Я могу сделать это в будущем, но хочу сначала решить эту проблему/недоразумение.)

НАСТРОЙКА СЕРВЕРОВ

Я не могу предоставить реальную услугу здесь, но это простая и надежная служба, которая возвращает XML в соответствии с URL-адресом, например:

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

Я хочу подключиться к URL-адресу через HTTPS с помощью GET и определить любые ошибки аутентификации (код состояния http 401).

Я подтвердил, что веб-сервис доступен, и что я могу успешно (http status code 200) захватить XML из URL с использованием указанного имени пользователя и пароля. Я сделал это с помощью веб-браузера и с 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. Я использую их, потому что свойство object объекта @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 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 и учетные данные верны.)

Любые предложения приветствуются!

4b9b3361

Ответ 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) {

 }];

Ответ 2

Подтвержденная проверка подлинности HTTP без проверки подлинности

Мне кажется, что вся документация по NSURLSession и HTTP Authentication пропускает тот факт, что требование аутентификации может быть запрошено (как в случае использования файла .htpassword) или unprompted (как это обычно бывает при работе с службой 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 и принудительно отключив NSAppTransportSecurity в файле проекта Info.plist (необходимый для проверки трафика SSL через прокси на iOS 9/OSX 10.11).

Ответ 3

Краткий ответ: поведение, которое вы описываете, согласуется с отказом аутентификации основного сервера. Я знаю, что вы сообщили, что вы подтвердили, что это правильно, но я подозреваю, что некоторые фундаментальные проблемы проверки на сервере (а не ваш код iOS).

Длинный ответ:

  • Если вы используете NSURLSession без делегата и включаете идентификатор пользователя/пароль в URL-адрес, тогда будет вызываться блок completionHandler NSURLSessionDataTask, если комбинация userid/password верна. Но, если аутентификация завершается неудачно, NSURLSession появляется, чтобы неоднократно пытаться сделать запрос, используя одни и те же учетные данные аутентификации каждый раз, а completionHandler, похоже, не вызван. (Я заметил, что, наблюдая связь с Charles Proxy).

    Это не кажется мне очень осмотрительным NSURLSession, но опять же отсутствие делегата не может действительно сделать намного больше. При использовании аутентификации использование подхода, основанного на delegate, кажется более надежным.

  • Если вы используете параметр NSURLSession с указанным параметром delegate (и no completionHandler при создании задачи данных), вы можете изучить характер ошибки в didReceiveChallenge, а именно изучить challenge.error и объектов challenge.failureResponse. Возможно, вы захотите обновить свой вопрос с этими результатами.

    Как и в стороне, вы, кажется, поддерживаете свой собственный счетчик _failureCount, но вместо этого вы, вероятно, можете воспользоваться свойством challenge.previousFailureCount.

  • Возможно, вы можете поделиться некоторыми сведениями о характере аутентификации, которую использует ваш сервер. Я только спрашиваю, потому что, когда я защищаю каталог на своем веб-сервере, он не вызывает метод 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 могут помочь нам определить, что происходит. Вы также можете ввести URL-адрес с идентификатором пользователя/паролем в веб-браузере, и это также может подтвердить, есть ли основная проблема с проверкой.