NSURLSession lifecycle and basic authorization

2020-06-04 11:37发布

问题:

I can't consistently read response from a server if I use code below.

Header:

#import <Foundation/Foundation.h>
@interface TestHttpClient : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>

-(void)POST:(NSString*) relativePath payLoad:(NSData*)payLoad;

@end

Implementation:

#import "TestHttpClient.h"

@implementation TestHttpClient

-(void)POST:(NSString*)relativePath payLoad:(NSData*)payLoad
{
    NSURL* url = [NSURL URLWithString:@"http://apps01.ditat.net/mobile/batch"];

    // Set URL credentials and save to storage
    NSURLCredential *credential = [NSURLCredential credentialWithUser:@"BadUser" password:@"BadPassword" persistence: NSURLCredentialPersistencePermanent];
    NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[url host] port:443 protocol:[url scheme] realm:@"Ditat mobile services endpoint" authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
    [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];

    // Configure session
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 30.0;
    sessionConfig.timeoutIntervalForResource = 60.0;
    sessionConfig.HTTPMaximumConnectionsPerHost = 1;
    sessionConfig.URLCredentialStorage = [NSURLCredentialStorage sharedCredentialStorage]; // Should this line be here??

    NSURLSession *session =     [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];

    // Create request object with parameters
    NSMutableURLRequest *request =
    [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60.0];

    // Set header data
    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/x-protobuf" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Version 1.0" forHTTPHeaderField:@"User-Agent"];
    [request setValue:@"Demo" forHTTPHeaderField:@"AccountId"];
    [request setValue:@"1234-5678" forHTTPHeaderField:@"DeviceSerialNumber"];
    [request setValue:@"iOS 7.1" forHTTPHeaderField:@"OSVersion"];
    [request setHTTPBody:payLoad];

    // Call session to post data to server??
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
    [downloadTask resume];
}

-(void)invokeDelegateWithResponse:(NSHTTPURLResponse *)response fileLocation:(NSURL*)location
{
    NSLog(@"HttpClient.invokeDelegateWithResponse - code %ld", (long)[response statusCode]);
}

#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"NSURLSessionDownloadDelegate.didFinishDownloadingToURL");
    [self invokeDelegateWithResponse:(NSHTTPURLResponse*)[downloadTask response] fileLocation:location];
    [session invalidateAndCancel];
}

// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
     didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}

// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}

Can be called from any VC (place code under button action for example)

-(IBAction)buttonTouchUp:(UIButton *)sender
{
    TestHttpClient *client = [[TestHttpClient alloc] init];
    [client POST:@"" payLoad:nil];
    return;
}

If you start program and call this code - it will show in NSLog completion with 401. Second try - will not work. Or might work if wait a little. But it won't send server requests as you push button.

NSURLSession somehow "remembers" failed attempts and won't return anything? Why is this behavior? I want to see 2 NSLog messages every time I push button.

回答1:

TL;DR; You are not handling authentication correctly in your example.

This is what happens when an iOS or MacOS client encounters a URL that requires authentication:

  1. The client requests a resource from the server GET www.example.com/protected

  2. The server response for that request has a status code of 401 and includes the WWW-Authenticate header. This tells the client this is a protected resource, and specifies what authentication method to use to access the resource. In iOS and MacOS, this is the "authentication challenge" that a delegate responds to. The WWW-Authenticate header is specifically mentioned in the documentation to highlight this.

    • Normally on iOS and MacOS, if a delegate is not provided or does not handle authentication challenges, the URL loading system will try to find an appropriate credential for this resource and authentication type by looking in NSURLCredentialStorage. It looks for a matching credential that has been saved as the default.

    • If a delegate implementing authentication IS provided, it's up to that delegate to provide a credential for that resource.

  3. When the system has a credential for the authentication challenge the request is attempted again with the credential.

    GET www.example.com/protected Authorization: Basic blablahaala

This explains the behavior you see in Charles, and is the correct behavior according to the various HTTP specifications.

Obviously, if you do not want to implement a delegate for your connection you have the option of putting a credential for the resource you are accessing in NSURLCredentialStorage. The system will use this, and will not require you to implement a delegate for the credential.

Create an NSURLCredential:

credential = [NSURLCredential credentialWithUser:@"some user" password:@"clever password" persistence: NSURLCredentialPersistencePermanent];

NSURLCredentialPersistencePermanent will tell NSURLCredentialStorage to store this permanently in the keychain. There are other possible values you can use, such as NSURLCredentialPersistenceForSession. These are covered in the documentation. . You should avoid using NSURLCredentialPersistencePermanent with credentials that have not been validated, use session or none until the credential has been validated. You have probably seen projects using 'KeychainWrapper' or directly accessing the Keychain API to save internet usernames and passwords - this is not the preferred way to do this, NSURLCredentialStorage is.

Create an NSURLProtectionSpace with the correct host, port, protocol, realm, and authentication method:

protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[url host] port:443 protocol:[url scheme] realm:@"Protected Area" authenticationMethod:NSURLAuthenticationMethodHTTPBasic];

Note that [[url port] integerValue] will not provide defaults for HTTP (80) or HTTPS (443). You must provide those. The realm MUST match what the server provides.

Finally, put it into NSURLCredentialStorage:

[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];

This will allow the URL loading system to use this credential from this point forward. Essentially the same process can also be used for a SSL/TLS server trust reference as well.

In your question you are handling server trust, but not the NSURLAuthenticationMethodHTTPBasic . When your application receives an authentication challenge for the HTTP Basic Auth, you are not responding to it, and things go downhill from there. In your case, you probably do not need to implement URLSession:didReceiveChallenge:completionHandler: at all if you do the steps above to set the default basic authentication credential for this protection space. The system will handle the NSURLAuthenticationMethodServerTrust by performing the default trust evaluation. The system will then find the default credential you set for this protection space for basic authentication and use that.

UPDATE

Based on new information in the comments, and running a modified version of the code, OP is actually getting this error in response to his request: NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) This error can be found in SecureTransport.h. The root certificate on the server credentials doesn't exist or is not trusted by the system. This is very rare, but can happen. Technote 2232 explains how to customize the server trust evaluation in the client to allow this certificate.