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 и учетные данные верны.)
Любые предложения приветствуются!
Ответы
Ответ 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-адрес с идентификатором пользователя/паролем в веб-браузере, и это также может подтвердить, есть ли основная проблема с проверкой.