URL Loading System
URL加载系统使用诸如标准的https或自定义协议,提供访问基于URL标识的资源的能力。加载是异步执行的,因此您的应用程序可以响应其他事件并在加载数据或错误到达时转回处理。
您可以使用NSURLSession
实例创建一个或多个NSURLSessionTask
实例,这些实例可以获取数据并将数据返回到应用程序,下载文件或将数据和文件上载到远程位置。要配置会话,请使用一个NSURLSessionConfiguration
对象,该对象控制如何使用cache和cookie,或者是否允许在蜂窝网络上进行连接。
您可以重复使用一个会话(session)来创建任务(task)。例如,Web浏览器可能使用多个会话,分别负责常规和隐私浏览,其中私有会话不提供缓存数据的能力。下图显示了具有这些配置的两个会话如何创建多个任务。
每个会话都与一个委托(delegate)相关联,以定期接收更新(或error)。默认委托调用您提供的completion handler block; 如果您选择提供自己的自定义delegate,则不会调用此block。
您可以将会话配置为在后台运行,以便在应用程序暂停时,系统可以代为下载数据并唤醒app以提供结果[1]。
NSURLSession
NSURLSession
: 协调一组相关网络数据传输任务的对象。
NSURLSession类和相关类提供下载内容的API。你可以使用NSURLSession API创建一个或多个会话,每个会话都会协调一组相关的数据传输任务[2]。
Session类型
NSURLSession拥有一个没有配置对象的单例sharedSession
,可用于处理基本请求。对于其他类型的session,你可以使用以下配置:
- 默认会话(Default session)类似共享session,允许更多的配置,并允许使用delegate获取增量数据。
- 临时会话(Ephemeral session)类似共享session,但不缓存cache,cookie或凭据到磁盘。
- 后台会话(Background session)可以在app未运行时在后台执行上传或下载内容的操作。
Task类型
在使用session的过程中,你可以通过创建task的方式完成上传、下载数据等操作。NSURLSession API提供了三种类型的任务:
- 数据任务(Data task)用于发送和接收
NSData
。 - 上传任务(Upload task)类似于数据任务,但也能用于发送文件数据,并支持在app未运行时后台上传。
- 下载任务(Download task)以文件的形式获取数据,同时支持后台下载。
使用delegate
Session中的task共享一个公共delegate,用于处理各种事件。如果不需要通过delegate处理事件,可以在创建session时传入nil
。
注意,NSURLSession
会强引用delegate
属性直到app退出或显式使session失效。如果你没有使session失效,会造成app内存泄漏。
@interface NSURLSession : NSObject
...
@property (nullable, readonly, retain) id delegate;
...
@end
线程安全性
URL Session API本身是完全线程安全的。您可以在任何线程上下文中自由创建session和task,并且当您的delegate方法调用completion handler时,工作将自动安排在正确的委托队列(delegate queue)中。
NSURLSessionTask
在URL会话中执行的任务,如下载特定资源。
NSURLSessionTask
类是session中task的基类,有三个子类可以使用:NSURLSessionDataTask
、NSURLSessionUploadTask
、NSURLSessionDownloadTask
。
不同于直接使用 alloc-init 初始化方法,task 是由一个NSURLSession
创建的。每个 task 的构造方法都有两个版本,一个带completionHandler
,一个不带,例如下面NSURLSessionDataTask
的构造方法。通过指定completionHandler
这个 block 将创建一个隐式的 delegate,来替代该 task 原来的 delegate——session。对于需要override 原有 session task 的 delegate 的默认行为的情况,我们需要使用这种不带completionHandler
的版本[3]。
/* Creates a data task with the given request. The request may have a body stream. */
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
/*
* data task convenience methods. These methods create tasks that
* bypass the normal delegate calls for response and data delivery,
* and provide a simple cancelable asynchronous interface to receiving
* data. Errors will be returned in the NSURLErrorDomain,
* see . The delegate, if any, will still be
* called for authentication challenges.
*/
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
所有的task都是可以取消,暂停或者恢复的。当一个 download task 取消时,可以通过选项来创建一个恢复数据(resume data),然后可以传递给下一次新创建的 download task,以便继续之前的下载。
注意,task的所有属性均支持KVO。
NSURLSessionConfiguration
用于定义URL会话的行为和策略的配置对象。
NSURLSessionConfiguration
对象用于对 NSURLSession
对象进行初始化。在初始化NSURLSession
对象之前,必须正确配置NSURLSessionConfiguration
对象,session 对象会 copy 一份配置数据,保存在自己的configuration
属性中。一旦初始化完成,再次修改NSURLSessionConfiguration
对象是不会影响到 session 对象的。
创建
NSURLSessionConfiguration
类提供了三个方法创建配置对象:
-
defaultSessionConfiguration
返回一个默认配置,具有相同的共享NSHTTPCookieStorage
,共享NSURLCache
和共享NSURLCredentialStorage
。 -
ephemeralSessionConfiguration
返回一个临时配置,不会对cache,cookie或证书进行缓存。 -
+backgroundSessionConfigurationWithIdentifier:
返回一个后台配置,可用于创建后台session,可以子啊app未运行时在后台执行上传和下载。初始化时指定的标识符用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。
可配置项
参考苹果官方文档。
处理鉴权查询(Authentication Challenge)
当服务器要求对URL请求进行身份验证时,适当地做出响应。
当客户端第一次发送请求的时候,服务器会返回一个包含公钥的受保护空间(也成为证书),当我们发送请求的时候,公钥会将请求加密再发送给服务器,服务器接到请求之后,用自带的私钥进行解密,如果正确再返回数据。这就是 HTTPS 的安全性所在[4]。
在把请求发送给服务器的过程中,服务器可能会发出鉴权查询(authentication challenge),这可以由共享的 cookie 或机密存储(credential storage)来自动响应,或者由被委托对象来响应。
你需要根据收到的鉴权查询的性质,决定实现下面哪个代理方法:
- 实现
NSURLSessionDelegate
协议的URLSession:didReceiveChallenge:completionHandler:
方法,用来处理session范围的鉴权查询,这些查询可能来自传输层安全协议(Transport Layer Security, TLS)验证。成功处理这种查询将对 session 内的所有 task 有效。 - 实现
NSURLSessionTaskDelegate
协议的URLSession:task:didReceiveChallenge:completionHandler
方法,用来处理特定 task 的鉴权查询,通常用于验证用户名/密码。Session 中的每个 task 都可以发起自己的鉴权查询。
/* If implemented, when a connection level authentication challenge
* has occurred, this delegate will be given the opportunity to
* provide authentication credentials to the underlying
* connection. Some types of authentication will apply to more than
* one request on a given connection to a server (SSL Server Trust
* challenges). If this delegate message is not implemented, the
* behavior will be to use the default handling, which may involve user
* interaction.
*/
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
/* The task has received a request specific authentication challenge.
* If this delegate is not implemented, the session specific authentication challenge
* will *NOT* be called and the behavior will be the same as using the default handling
* disposition.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
注意,从苹果的官方文档可以获取所有鉴权查询的类型。
当 app 使用 https 协议发送请求时,NSURLSessionDelegate
会收到NSURLAuthenticationMethodServerTrust
类型的鉴权查询。不同于服务器要求验证你的app的身份,这个类型的鉴权查询提供了使你验证服务器证书的机会。大多数情况下,你应该让系统自动处理。除非你希望接受原本会被系统拒绝的服务器证书,或希望拒绝某个被系统接受的证书。
下面的代码展示了如何访问服务器证书,以及如何验证证书并接受或拒绝它:
- 如果证书有效,使用
serverTrust
创建一个NSURLCredential
实例,然后调用completionHandler()
,传入NSURLSessionAuthChallengeUseCredential
和 credential 对象。这样系统将接受服务器证书。 - 如果证书无效,调用
completionHandler()
,传入NSURLSessionAuthChallengeCancelAuthenticationChallenge
和nil
。这样系统将拒绝服务器证书。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
completionHandler(disposition, credential);
return;
}
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType checkResult;
OSStatus trustEvalStatus = SecTrustEvaluate(trust, &checkResult);
if (trustEvalStatus == errSecSuccess
&& (checkResult == kSecTrustResultProceed
|| checkResult == kSecTrustResultUnspecified)) {
credential = [NSURLCredential credentialForTrust:trust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
else {
// Show a UI here warning the user the server credentials are invalid, and cancel the load.
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
completionHandler(disposition, credential);
}
注意,参考苹果官方文档,了解如何验证SecTrustRef
实例或从中访问证书或实例的详细信息。
NSURLConnection vs. NSURLSession
NSURLSession
的 delegate 方法是 NSURLConnection
的演化的十年中对于 ad-hoc 模式的一个显著改善。您可以查看这个映射表来进行一个完整的概览。
ATS
在 Apple 平台上,应用程序和应用程序扩展可以使用名为App Transport Security(ATS)的网络安全功能,默认情况下已启用。它通过确保您的应用程序的网络连接仅使用行业标准协议和没有已知缺陷的密码,从而提高了隐私和数据完整性。这有助于让用户相信您的应用程序不会意外地将传输的数据泄露给恶意方。
ATS 基本配置
通过在应用程序Info.plist
文件中配置NSAppTransportSecurity
键的方式自定义网络连接的安全性。
默认情况下 ATS 在 iOS 9 和 macOS 10.11 及以后的系统上默认为开启状态。启用 ATS 后,所有的http请求必须使用HTTPS协议,尝试连接不安全的HTTP协议将会失败。ATS使用Transport Layer Security(TLS),协议版本1.2
。
以下列表展示了NSAppTransportSecurity
字典的整体结构,显示了所有可能的键,所有的键都是可选(optional)的[5]:
NSAppTransportSecurity : Dictionary {
NSAllowsArbitraryLoads : Boolean
NSAllowsArbitraryLoadsForMedia : Boolean
NSAllowsArbitraryLoadsInWebContent : Boolean
NSAllowsLocalNetworking : Boolean
NSExceptionDomains : Dictionary {
: Dictionary {
NSIncludesSubdomains : Boolean
NSExceptionAllowsInsecureHTTPLoads : Boolean
NSExceptionMinimumTLSVersion : String
NSExceptionRequiresForwardSecrecy : Boolean // Default value is YES
NSRequiresCertificateTransparency : Boolean
}
}
}
自定义配置
有以下几种常见的自定义配置方式可供使用[6]:
全部启用 ATS
这是默认状态,直接使用 NSURLSession,NSURLConnection,CFURL 而不需要做任何配置。当然只能在 iOS 9 和 macOS 10.11 及以后的系统上有效。
全部启用 ATS,但有一些例外
如果需要向不使用 ATS 的域发送请求,需要将这些域加入到NSExceptionDomains
字典中。如果希望该域的所有子域全部禁用 ATS,需要设置NSExceptionAllowsInsecureHTTPLoads
为YES
,同时将NSIncludesSubdomains
设置为YES
。
全部禁用 ATS,但有一些例外
如果需要全部禁用 ATS,需要设置NSAllowsArbitraryLoads
为YES
,则所有的请求将不会使用 ATS。如果希望在一些例外的域使用 ATS,需要将这些域加入NSExceptionDomains
字典。每个希望启用 ATS 的域都要在字典中将NSExceptionAllowsInsecureHTTPLoads
设置为NO
。
降级的 ATS
某些情况下你可能需要全部启用 ATS,但实际上并没有完全支持 ATS 的最佳实践。例如服务器支持 TLS1.2,但不支持前向保密(forward secrecy)。为了解决这个问题,可以使指定于支持 ATS,同时禁用前向保密,需要在NSExceptionDomains
字典中加入该域,同时设置域的NSExceptionRequiresForwardSecrecy
为NO
。
同样,如果需要支持前向保密,而TLS版本只有1.1
,则需要设置NSExceptionMinimumTLSVersion
为TLSv1.1
。
全部禁用 ATS
如果需要全部禁用 ATS,只需将NSAllowsArbitraryLoads
设置为YES
。
-
URL Loading System ↩
-
NSURLSession ↩
-
从 NSURLConnection 到 NSURLSession ↩
-
iOS - HTTPS ↩
-
NSAppTransportSecurity ↩
-
WORKING WITH APPLE’S APP TRANSPORT SECURITY ↩