iOS/Cocoa - NSURLSession - базовая HTTPS-авторизация
[отредактировано, чтобы предоставить больше информации]
(Я не использую AFNetworking для этого проекта. Я могу сделать это в будущем, но сначала хочу разрешить эту проблему / недоразумение.)
НАСТРОЙКА СЕРВЕРА
Я не могу предоставить реальный сервис здесь, но это простой, надежный сервис, который возвращает XML в соответствии с URL-адресом, таким как:
https://username:password@example.com/webservice
Я хочу подключиться к URL через HTTPS с помощью GET и определить любые ошибки аутентификации (http status code 401).
Я подтвердил, что веб-служба доступна и что я могу (http код состояния 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. Я использую их, потому что свойство @previousFailureCount объекта вызова никогда не продвигается! Он всегда остается равным нулю, независимо от того, сколько раз вызывались эти методы обратного вызова.
ПРОБЛЕМА 2 ПРИ ИСПОЛЬЗОВАНИИ ПОПЫТКИ
Несмотря на использование правильных учетных данных (что подтверждается их успешным использованием с нулевым делегатом), проверка подлинности не выполняется.
Следующие обратные вызовы происходят:
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 и учетные данные верны.)
Любые предложения приветствуются!
3 ответа
Вам не нужно реализовывать метод делегата для этого, просто установите 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-аутентификация
Мне кажется, что вся документация по 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 и принудительного отключения NSAppTransportSecurity
в моем проекте Info.plist
файл (необходим для проверки трафика SSL через прокси на iOS 9/OSX 10.11).
Краткий ответ. Описанное вами поведение согласуется с ошибкой базовой аутентификации сервера. Я знаю, что вы сообщили, что вы убедились, что это правильно, но я подозреваю, что существует некоторая фундаментальная проблема проверки на сервере (не ваш код iOS).
Длинный ответ:
Если вы используете
NSURLSession
без делегата и включите идентификатор пользователя / пароль в URL, затемcompletionHandler
блок изNSURLSessionDataTask
будет вызван, если комбинация ID пользователя и пароля правильная. Но если аутентификация не удалась,NSURLSession
появляется многократная попытка сделать запрос, каждый раз используя одни и те же учетные данные, иcompletionHandler
не кажется, чтобы позвонить. (Я заметил это, наблюдая за связью с Чарльзом Прокси).Это не кажется мне очень благоразумным
NSURLSession
, но опять же, представление без делегатов не может сделать намного больше, чем это. При использовании аутентификации, используяdelegate
основанный на подходе кажется более надежным.Если вы используете
NSURLSession
сdelegate
указано (и нет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-адрес с идентификатором пользователя / паролем в веб-браузере, и это также может подтвердить, есть ли проблема с базовой аутентификацией.