【问题标题】:NSURLSession lifecycle and basic authorizationNSURLSession 生命周期和基本授权
【发布时间】:2014-07-29 09:52:00
【问题描述】:

如果我使用以下代码,我无法始终从服务器读取响应。

标题:

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

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

@end

实施:

#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
{
}

可以从任何 VC 调用(例如在按钮操作下放置代码)

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

如果您启动程序并调用此代码 - 它会在 NSLog 完成中显示 401。第二次尝试 - 将不起作用。或者如果稍等一下可能会起作用。但它不会在您按下按钮时发送服务器请求。

NSURLSession 以某种方式“记住”失败的尝试并且不会返回任何内容?为什么会出现这种行为?每次按下按钮时,我都希望看到 2 条 NSLog 消息。

【问题讨论】:

  • 您需要实现自己的包装器,还是可以使用现有的框架? AFNetworking 是个不错的选择。即使你不能使用它,你也可以在它的实现中找到答案。
  • AFNetworking 也是一个包装器,怀疑它是否能解决我的问题,只需添加另一层代码。我更喜欢自己弄清楚而不使用 3rd 方库,尤其是我所做的只是下载文件..我猜它以某种方式缓存了响应,我只需要编写某种强制清理..
  • 就总是看到两个请求(而不仅仅是第一次)而言,这可能是因为您正在为每个请求创建一个新的NSURLSession。您真的想创建一个 NSURLSession 并让后续请求使用同一个会话。
  • 可能与您手头的问题无关,但您的didReceiveChallenge 应该有一个else 条件,该条件使用NSURLSessionAuthChallengeCancelAuthenticationChallenge 调用completionHandler。如果 if 语句失败,如果您根本不调用 completionHandler,您当前的实现将停止。
  • Rob,我在单个请求中看到双打,这只是内置客户端的工作方式。我在 .NET 中看到它的行为相同

标签: ios nsurlsession


【解决方案1】:

TL;DR;您的示例中没有正确处理身份验证。

当 iOS 或 MacOS 客户端遇到需要身份验证的 URL 时会发生这种情况:

  1. 客户端向服务器请求资源 GET www.example.com/protected

  2. 该请求的服务器响应的状态代码为 401,并包含 WWW-Authenticate 标头。这告诉客户端这是一个受保护的资源,并指定使用什么身份验证方法来访问该资源。在 iOS 和 MacOS 中,这是委托响应的“身份验证挑战”。 The WWW-Authenticate header is specifically mentioned in the documentation to highlight this.

    • 通常在 iOS 和 MacOS 上,如果未提供委托或不处理身份验证挑战,则 URL 加载系统将尝试通过查找 NSURLCredentialStorage 为该资源和身份验证类型找到适当的凭据。它会查找已保存为默认值的匹配凭据。

    • 如果提供了实现身份验证的委托,则由该委托提供该资源的凭据。

  3. 当系统具有用于身份验证质询的凭据时,将使用该凭据再次尝试请求。

    获取 www.example.com/protected 授权:基本的blablahaala

这解释了您在 Charles 中看到的行为,并且是根据各种 HTTP 规范的正确行为。

显然,如果您不想为您的连接实现委托,您可以选择将您正在访问的资源的凭据放在 NSURLCredentialStorage 中。系统将使用它,并且不需要您为凭证实现委托。

创建一个 NSURLCredential:

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

NSURLCredentialPersistencePermanent 将告诉NSURLCredentialStorage 将其永久存储在钥匙串中。您可以使用其他可能的值,例如NSURLCredentialPersistenceForSessionThese are covered in the documentation. 。您应该避免将NSURLCredentialPersistencePermanent 与未经验证的凭据一起使用,在验证凭据之前使用 session 或 none。您可能已经看到使用“KeychainWrapper”或直接访问 Keychain API 来保存 Internet 用户名和密码的项目 - 这不是执行此操作的首选方式,NSURLCredentialStorage 是。

使用正确的主机、端口、协议、领域和身份验证方法创建一个NSURLProtectionSpace

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

请注意,[[url port] integerValue] 不会为 HTTP (80) 或 HTTPS (443) 提供默认值。你必须提供这些。领域必须与服务器提供的匹配。

最后放入NSURLCredentialStorage

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

这将允许 URL 加载系统从现在开始使用此凭据。本质上,同样的过程也可用于 SSL/TLS 服务器信任引用。

在您的问题中,您正在处理服务器信任,但不是 NSURLAuthenticationMethodHTTPBasic当您的应用程序收到 HTTP 基本身份验证的身份验证质询时,您没有响应它,事情从那里开始走下坡路。 在您的情况下,您可能根本不需要实现 URLSession:didReceiveChallenge:completionHandler:如果您执行上述步骤来设置此保护空间的默认基本身份验证凭据。系统将通过执行默认信任评估来处理NSURLAuthenticationMethodServerTrust。然后,系统将找到您为此保护空间设置的默认凭据以进行基本身份验证并使用它。

更新

根据 cmets 中的新信息,并运行代码的修改版本,OP 实际上收到此错误以响应他的请求: NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 此错误可在 SecureTransport.h 中找到。服务器凭据上的根证书不存在或不受系统信任。这是非常罕见的,但可能会发生。 Technote 2232 解释了如何将客户端中的服务器信任评估自定义为allow this certificate

【讨论】:

  • 您也应该删除您的委托的身份验证相关方法。在这些和设置您自己的身份验证标头之间,这是您的问题。当它无法进行身份验证时,您要么阻止它返回错误,要么您没有看到/处理错误。
  • 再次,在 didReceiveChallenge 中,您正在处理服务器信任挑战者,而不是基本身份验证挑战。它进入了一个循环,因为服务器告诉客户端进行身份验证,但您没有处理挑战。您根本不需要 didReceiveChallenge 即可。
  • 1.您没有执行任何不同于系统默认值的服务器信任评估。 2. 您根本没有在您的委托中执行任何基本身份验证,并且实施 didReceiveChallenge 会阻止系统执行此操作。您可以删除此委托方法,系统将同时执行这两个挑战。
  • 服务器信任不需要单独进行,除非您需要根据 Technote 2232 自定义服务器信任评估。您没有在实施中进行 any 信任评估,因此我假设您不需要自定义信任评估。如果您使用的是自签名证书等,您会这样做。您在实施中的服务器信任评估只是寻找现有的凭证。它不评估服务器信任。
  • 发生这种情况时,您从会话中返回什么错误?你在处理错误吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-06
  • 2014-01-15
  • 1970-01-01
  • 1970-01-01
  • 2021-01-03
相关资源
最近更新 更多