NSURLSessionでBasic認証する

どうせ1つのアプリで複数のBasic認証することなんて無いっしょ
とか思いつつも使い回しがききそうな実装を考えたのでメモ

  1. requestのURLはhttp://user:password@example.comみたいにユーザとパスワードも記載しておくとする
  2. NSURLSessionTaskcompletionHandlerは使わないでdelegateを使用する
  3. WWW-Authenticaterealmを取得
  4. 認証するURLに対してcurl -vとかリクエストして401レスポンスを取得
  5. 今回はこんなレスポンスが来ていたWWW-Authenticate: Basic realm="Login Required"
  6. 該当realmLogin Required
  7. delegateメソッド- URLSession:didReceiveChallenge:completionHandler:を実装する
  8. - URLSession:didReceiveChallenge:completionHandler:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    /*
     Basic認証処理
     */
    NSURL *url = task.originalRequest.URL;
    
    // 認証情報ストレージを取得
    NSURLCredentialStorage *storage = [NSURLCredentialStorage sharedCredentialStorage];
    // 認証情報ストレージのキーとなるNSURLProtectionSpaceを作成
    NSURLProtectionSpace *ps = [[NSURLProtectionSpace alloc] initWithHost:url.host
                                                                     port:[url.port integerValue]
                                                                 protocol:url.scheme
                                                                    realm:@"Login Required"
                                                     authenticationMethod:NSURLAuthenticationMethodDefault];
    // 認証情報を取得
    NSURLCredential *credential = [storage defaultCredentialForProtectionSpace:ps];
    if (!credential) {
        // 認証情報を作成、保存する
        credential = [[NSURLCredential alloc] initWithUser:url.user
                                                  password:url.password
                                               persistence:NSURLCredentialPersistenceForSession];
        [storage setCredential:credential forProtectionSpace:ps];
    }
    
    if ([challenge previousFailureCount]>0) {
        // 1度でも認証失敗している場合はキャンセルする
        [task cancel];
        [session invalidateAndCancel];
    }
    else {
        // 認証リクエスト
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeUseCredential;
        completionHandler(disposition, credential);
    }
}

追記

basic認証の必要なAPIリクエストを並列実行していたら- setCredential:forProtectionSpace:でクラッシュしたので多分書き込みバッティングかも  
対策としてはNSOperationQueueを使ってもよかったけど、それはそれで面倒だったのでdispatch_queue_tを返すクラスメソッド作ってdispatch_barrier_asyncで実行してあげて無理矢理同期っぽいバッティング回避策をすることをした(適当)

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    dispatch_barrier_async([[self class] queue], [^{
    	// 認証処理
    });
}

+ (dispatch_queue_t)queue {
    static dispatch_queue_t queue = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
        queue = dispatch_queue_create([[NSString stringWithFormat:@"%@.network", bundleId] UTF8String], NULL);
    });
    return queue;
}