web-dev-qa-db-ja.com

iOS / Cocoa-NSURLSession-基本的なHTTPS認証の処理

[より多くの情報を提供するために編集]

(このプロジェクトではAFNetworkingを使用していません。将来使用する可能性がありますが、この問題/誤解を最初に解決したいと考えています。)

サーバー設定

ここでは実際のサービスを提供することはできませんが、次のようなURLに従ってXMLを返すシンプルで信頼性の高いサービスです。

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

GETを使用してHTTPS経由でURLに接続し、認証の失敗(httpステータスコード401)を確認します。

Webサービスが利用可能であり、指定したユーザー名とパスワードを使用してURLからXMLを正常に取得できる(httpステータスコード200)ことを確認しました。私はこれを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)が提供されず、最終的にタスクがタイムアウトします。

ATTEMPTED SOLUTION

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を使用する場合の問題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と資格情報が正しいことを示しています。)

どんな提案も歓迎します!

16
Womble

これにデリゲートメソッドを実装する必要はありません。リクエストに認証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) {

 }];
30
malhal

プロンプトとプロンプトなしのHTTP認証

NSURLSessionとHTTP認証に関するすべてのドキュメントは、認証の要件がpromptedである可能性があるという事実をスキップしているようです( .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 を使用して、プロジェクトのInfo.plistファイルのNSAppTransportSecurityを強制的に無効にしました(iOS 9のプロキシ経由でSSLトラフィックを検査するために必要)/OSX 10.11)。

22
markeissler

短い答え:あなたが説明する動作は、基本的なサーバー認証の失敗と一致しています。正しいことが確認されたとのことですが、iOSコードではなく、サーバー上に根本的な検証の問題があると思います。

長い答え:

  1. デリゲートなしでNSURLSessionを使用し、URLにユーザーID /パスワードを含める場合、ユーザーID /パスワードの組み合わせが正しい場合、completionHandlerNSURLSessionDataTaskブロックが呼び出されます。ただし、認証が失敗した場合、NSURLSessionは毎回同じ認証資格情報を使用してリクエストを繰り返し試行しているように見え、completionHandlerは呼び出されていないようです。 (私は Charles Proxy との接続を監視することに気づきました)。

    これはNSURLSessionの賢明さには影響しませんが、デリゲートなしのレンディションではそれ以上のことはできません。認証を使用する場合、delegateベースのアプローチを使用すると、より堅牢に見えます。

  2. NSURLSessiondelegateを指定して使用し、データタスクの作成時にcompletionHandlerパラメーターを使用しない場合、didReceiveChallenge、つまりchallenge.error そしてその challenge.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を入力すると、基本認証の問題があるかどうかも確認できます。

10
Rob