afnetworking2.0源码解析

来源:互联网 发布:linux压缩解压缩命令 编辑:程序博客网 时间:2024/05/07 04:07
最近看AFNetworking2的源码,学习这个知名网络框架的实现,顺便梳理写下文章。AFNetworking的代码还在不断更新中,我看的是AFNetworking2.3.1
 
本篇先看看AFURLConnectionOperation,AFURLConnectionOperation继承自NSOperation,是一个封装好的任务单元,在这里构建了NSURLConnection,作为NSURLConnection的delegate处理请求回调,做好状态切换,线程管理,可以说是AFNetworking最核心的类,下面分几部分说下看源码时注意的点,最后放上代码的注释。
 
0.Tricks
AFNetworking代码中有一些常用技巧,先说明一下。
 
A.clang warning
  1. #pragma clang diagnostic push 
  2. #pragma clang diagnostic ignored "-Wgnu" 
  3. //code 
  4. #pragma clang diagnostic pop 
表示在这个区间里忽略一些特定的clang的编译警告,因为AFNetworking作为一个库被其他项目引用,所以不能全局忽略clang的一些警告,只能在有需要的时候局部这样做,作者喜欢用?:符号,所以经常见忽略-Wgnu警告的写法,详见这里
 
B.dispatch_once
为保证线程安全,所有单例都用dispatch_once生成,保证只执行一次,这也是iOS开发常用的技巧。例如:
  1. static dispatch_queue_t url_request_operation_completion_queue() { 
  2.     static dispatch_queue_t af_url_request_operation_completion_queue; 
  3.     static dispatch_once_t onceToken; 
  4.     dispatch_once(&onceToken, ^{ 
  5.         af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue",   DISPATCH_QUEUE_CONCURRENT ); 
  6.     }); 
  7.     return af_url_request_operation_completion_queue; 
C.weak & strong self
常看到一个 block 要使用 self,会处理成在外部声明一个 weak 变量指向 self,在 block 里又声明一个 strong 变量指向 weakSelf:
  1. __weak __typeof(self)weakSelf = self; 
  2. self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ 
  3.     __strong __typeof(weakSelf)strongSelf = weakSelf; 
  4. }]; 
weakSelf是为了block不持有self,避免循环引用,而再声明一个strongSelf是因为一旦进入block执行,就不允许self在这个执行过程中释放。block执行完后这个strongSelf会自动释放,没有循环引用问题。

1.线程
先来看看 NSURLConnection 发送请求时的线程情况,NSURLConnection 是被设计成异步发送的,调用了start方法后,NSURLConnection 会新建一些线程用底层的 CFSocket 去发送和接收请求,在发送和接收的一些事件发生后通知原来线程的Runloop去回调事件。
 
NSURLConnection 的同步方法 sendSynchronousRequest 方法也是基于异步的,同样要在其他线程去处理请求的发送和接收,只是同步方法会手动block住线程,发送状态的通知也不是通过 RunLoop 进行。
 
使用NSURLConnection有几种选择:
 
A.在主线程调异步接口
若直接在主线程调用异步接口,会有个Runloop相关的问题:
 
当在主线程调用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 时,请求发出,侦听任务会加入到主线程的 Runloop 下,RunloopMode 会默认为 NSDefaultRunLoopMode。这表明只有当前线程的Runloop 处于 NSDefaultRunLoopMode 时,这个任务才会被执行。但当用户滚动 tableview 或 scrollview 时,主线程的 Runloop 是处于 NSEventTrackingRunLoopMode 模式下的,不会执行 NSDefaultRunLoopMode 的任务,所以会出现一个问题,请求发出后,如果用户一直在操作UI上下滑动屏幕,那在滑动结束前是不会执行回调函数的,只有在滑动结束,RunloopMode 切回 NSDefaultRunLoopMode,才会执行回调函数。苹果一直把动画效果性能放在第一位,估计这也是苹果提升UI动画性能的手段之一。
 
所以若要在主线程使用 NSURLConnection 异步接口,需要手动把 RunloopMode 设为 NSRunLoopCommonModes。这个 mode 意思是无论当前 Runloop 处于什么状态,都执行这个任务。
  1. NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; 
  2. [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
  3. [connection start]; 
B.在子线程调同步接口
若在子线程调用同步接口,一条线程只能处理一个请求,因为请求一发出去线程就阻塞住等待回调,需要给每个请求新建一个线程,这是很浪费的,这种方式唯一的好处应该是易于控制请求并发的数量。
 
C.在子线程调异步接口
子线程调用异步接口,子线程需要有 Runloop 去接收异步回调事件,这里也可以每个请求都新建一条带有 Runloop 的线程去侦听回调,但这一点好处都没有,既然是异步回调,除了处理回调内容,其他时间线程都是空闲可利用的,所有请求共用一个响应的线程就够了。
 
AFNetworking 用的就是第三种方式,创建了一条常驻线程专门处理所有请求的回调事件,这个模型跟 nodejs 有点类似。网络请求回调处理完,组装好数据后再给上层调用者回调,这时候回调是抛回主线程的,因为主线程是最安全的,使用者可能会在回调中更新UI,在子线程更新UI会导致各种问题,一般使用者也可以不需要关心线程问题。
 
以下是相关线程大致的关系,实际上多个 NSURLConnection 会共用一个 NSURLConnectionLoader 线程,这里就不细化了,除了处理 socket 的 CFSocket 线程,还有一些 Javascript:Core 的线程,目前不清楚作用,归为 NSURLConnection里的其他线程。因为 NSURLConnection 是系统控件,每个iOS版本可能都有不一样,可以先把 NSURLConnection 当成一个黑盒,只管它的 start 和 callback 就行了。如果使用 AFHttpRequestOperationManager 的接口发送请求,这些请求会统一在一个 NSOperationQueue 里去发,所以多了上面 NSOperationQueue 的一个线程。
 
 
相关代码:-networkRequestThread:, -start:, -operationDidStart:。
 
2.状态机
继承 NSOperation 有个很麻烦的东西要处理,就是改变状态时需要发 KVO 通知,否则这个类加入 NSOperationQueue 不可用了。 NSOperationQueue 是用 KVO 方式侦听 NSOperation 状态的改变,以判断这个任务当前是否已完成,完成的任务需要在队列中除去并释放。
 
AFURLConnectionOperation 对此做了个状态机,统一搞定状态切换以及发 KVO 通知的问题,内部要改变状态时,就只需要类似 self.state = AFOperationReadyState 的调用而不需要做其他了,状态改变的 KVO 通知在 setState 里发出。
总的来说状态管理相关代码就三部分,一是限制一个状态可以切换到其他哪些状态,避免状态切换混乱,二是状态 Enum值 与 NSOperation 四个状态方法的对应,三是在 setState 时统一发 KVO 通知。详见代码注释。
 
相关代码:AFKeyPathFromOperationState, AFStateTransitionIsValid, -setState:, -isPaused:, -isReady:, -isExecuting:, -isFinished:.

3.NSURLConnectionDelegate
处理 NSURLConnection Delegate 的内容不多,代码也是按请求回调的顺序排列下去,十分易读,主要流程就是接收到响应的时候打开 outputStream,接着有数据过来就往 outputStream 写,在上传/接收数据过程中会回调上层传进来的相应的callback,在请求完成回调到 connectionDidFinishLoading 时,关闭 outputStream,用 outputStream 组装 responseData 作为接收到的数据,把 NSOperation 状态设为 finished,表示任务完成,NSOperation 会自动调用 completeBlock,再回调到上层。
 
4.setCompleteBlock
NSOperation 在 iOS4.0 以后提供了个接口 setCompletionBlock,可以传入一个 block 作为任务执行完成时(state状态机变为finished时)的回调,AFNetworking直接用了这个接口,并通过重写加了几个功能:
 
A.消除循环引用
在 NSOperation 的实现里,completionBlock 是 NSOperation 对象的一个成员,NSOperation 对象持有着 completionBlock,若传进来的 block 用到了 NSOperation 对象,或者 block 用到的对象持有了这个 NSOperation 对象,就会造成循环引用。这里执行完 block 后调用 [strongSelf setCompletionBlock:nil] 把 completionBlock 设成 nil,手动释放 self(NSOperation对象) 持有的 completionBlock 对象,打破循环引用。
 
可以理解成对外保证传进来的block一定会被释放,解决外部使用使很容易出现的因对象关系复杂导致循环引用的问题,让使用者不知道循环引用这个概念都能正确使用。

B.dispatch_group
这里允许用户让所有 operation 的 completionBlock 在一个 group 里执行,但我没看出这样做的作用,若想组装一组请求(见下面的batchOfRequestOperations)也不需要再让completionBlock在group里执行,求解。

C.”The Deallocation Problem”
作者在注释里说这里重写的setCompletionBlock方法解决了”The Deallocation Problem”,实际上并没有。”The Deallocation Problem”简单来说就是不要让UIKit的东西在子线程释放。
 
这里如果传进来的block持有了外部的UIViewController或其他UIKit对象(下面暂时称为A对象),并且在请求完成之前其他所有对这个A对象的引用都已经释放了,那么这个completionBlock就是最后一个持有这个A对象的,这个block释放时A对象也会释放。这个block在什么线程释放,A对象就会在什么线程释放。我们看到block释放的地方是url_request_operation_completion_queue(),这是AFNetworking特意生成的子线程,所以按理说A对象是会在子线程释放的,会导致UIKit对象在子线程释放,会有问题。
 
但AFNetworking实际用起来却没问题,想了很久不得其解,后来做了实验,发现iOS5以后苹果对UIKit对象的释放做了特殊处理,只要发现在子线程释放这些对象,就自动转到主线程去释放,断点出来是由一个叫_objc_deallocOnMainThreadHelper 的方法做的。如果不是UIKit对象就不会跳到主线程释放。AFNetworking2.0只支持iOS6+,所以没问题。
 
 
5.batchOfRequestOperations
这里额外提供了一个便捷接口,可以传入一组请求,在所有请求完成后回调 complionBlock,在每一个请求完成时回调 progressBlock 通知外面有多少个请求已完成。详情参见代码注释,这里需要说明下 dispatch_group_enter 和dispatch_group_leave 的使用,这两个方法用于把一个异步任务加入 group 里。
 
一般我们要把一个任务加入一个group里是这样:
  1. dispatch_group_async(group, queue, ^{ 
  2.     block(); 
  3. }); 
这个写法等价于
  1. dispatch_async(queue, ^{ 
  2.     dispatch_group_enter(group); 
  3.     block() 
  4.     dispatch_group_leave(group); 
  5. }); 
如果要把一个异步任务加入group,这样就行不通了:
  1. dispatch_group_async(group, queue, ^{ 
  2.     [self performBlock:^(){ 
  3.         block(); 
  4.     }]; 
  5.     //未执行到block() group任务就已经完成了 
  6. }); 
这时需要这样写:
  1. dispatch_group_enter(group); 
  2. [self performBlock:^(){ 
  3.     block(); 
  4.     dispatch_group_leave(group); 
  5. }]; 
异步任务回调后才算这个group任务完成。对batchOfRequest的实现来说就是请求完成并回调后,才算这个任务完成。
 
其实这跟retain/release差不多,都是计数,dispatch_group_enter时任务数+1,dispatch_group_leave时任务数-1,任务数为0时执行dispatch_group_notify的内容。
 
相关代码:-batchOfRequestOperations:progressBlock:completionBlock:
 
6.其他
A.锁
AFURLConnectionOperation 有一把递归锁,在所有会访问/修改成员变量的对外接口都加了锁,因为这些对外的接口用户是可以在任意线程调用的,对于访问和修改成员变量的接口,必须用锁保证线程安全。
 
B.序列化
AFNetworking 的多数类都支持序列化,但实现的是 NSSecureCoding 的接口,而不是 NSCoding,区别在于解数据时要指定 Class,用 -decodeObjectOfClass:forKey: 方法代替了 -decodeObjectForKey: 。这样做更安全,因为序列化后的数据有可能被篡改,若不指定 Class,-decode 出来的对象可能不是原来的对象,有潜在风险。另外,NSSecureCoding 是 iOS 6 以上才有的。详见这里
 
这里在序列化时保存了当前任务状态,接收的数据等,但回调block是保存不了的,需要在取出来发送时重新设置。可以像下面这样持久化保存和取出任务:
  1. AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 
  2. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:operation]; 
  3.   
  4. AFHTTPRequestOperation *operationFromDB = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 
  5. [operationFromDB start]; 
C.backgroundTask
这里提供了setShouldExecuteAsBackgroundTaskWithExpirationHandler 接口,决定APP进入后台后是否继续发送接收请求,并在后台执行时间超时后取消所有请求。在 dealloc 里需要调用 [application endBackgroundTask:] ,告诉系统这个后台任务已经完成,不然系统会一直让你的APP运行在后台,直到超时。
 
相关代码:-setShouldExecuteAsBackgroundTaskWithExpirationHandler:, -dealloc:

7.AFHTTPRequestOperation
AFHTTPRequestOperation 继承了 AFURLConnectionOperation,把它放一起说是因为它没做多少事情,主要多了responseSerializer,暂停下载断点续传,以及提供接口请求成功失败的回调接口 -setCompletionBlockWithSuccess:failure:。详见源码注释。

本篇我们继续来看看AFNetworking的下一个模块 — AFURLRequestSerialization。
 
AFURLRequestSerialization用于帮助构建NSURLRequest,主要做了两个事情:
 
1.构建普通请求:格式化请求参数,生成HTTP Header。
2.构建multipart请求。
 
分别看看它在这两点具体做了什么,怎么做的。
 
1.构建普通请求
 
A.格式化请求参数
一般我们请求都会按key=value的方式带上各种参数,GET方法参数直接加在URL上,POST方法放在body上,NSURLRequest没有封装好这个参数的解析,只能我们自己拼好字符串。AFNetworking提供了接口,让参数可以是NSDictionary, NSArray, NSSet这些类型,再由内部解析成字符串后赋给NSURLRequest。
 
转化过程大致是这样的:
  1. @{ 
  2.      @"name" : @"bang"
  3.      @"phone": @{@"mobile": @"xx", @"home": @"xx"}, 
  4.      @"families": @[@"father", @"mother"], 
  5.      @"nums": [NSSet setWithObjects:@"1", @"2", nil] 
  6. -> 
  7. @[ 
  8.      field: @"name", value: @"bang"
  9.      field: @"phone[mobile]", value: @"xx"
  10.      field: @"phone[home]", value: @"xx"
  11.      field: @"families[]", value: @"father"
  12.      field: @"families[]", value: @"mother"
  13.      field: @"nums", value: @"1"
  14.      field: @"nums", value: @"2"
  15. -> 
  16. name=bang&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2 
第一部分是用户传进来的数据,支持包含NSArray,NSDictionary,NSSet这三种数据结构。
 
第二部分是转换成AFNetworking内自己的数据结构,每一个key-value对都用一个对象AFQueryStringPair表示,作用是最后可以根据不同的字符串编码生成各自的key=value字符串。主要函数是AFQueryStringPairsFromKeyAndValue,详见源码注释。
 
第三部分是最后生成NSURLRequest可用的字符串数据,并且对参数进行url编码,在AFQueryStringFromParametersWithEncoding这个函数里。
 
最后在把数据赋给NSURLRequest时根据不同的HTTP方法分别处理,对于GET/HEAD/DELETE方法,把参数加到URL后面,对于其他如POST/PUT方法,把数据加到body上,并设好HTTP头,告诉服务端字符串的编码。
 
B.HTTP Header
AFNetworking帮你组装好了一些HTTP请求头,包括语言Accept-Language,根据 [NSLocale preferredLanguages] 方法读取本地语言,高速服务端自己能接受的语言。还有构建 User-Agent,以及提供Basic Auth 认证接口,帮你把用户名密码做 base64 编码后放入 HTTP 请求头。详见源码注释。
 
C.其他格式化方式
HTTP请求参数不一定是要key=value形式,可以是任何形式的数据,可以是json格式,苹果的plist格式,二进制protobuf格式等,AFNetworking提供了方法可以很容易扩展支持这些格式,默认就实现了json和plist格式。详见源码的类AFJSONRequestSerializer和AFPropertyListRequestSerializer。
 
2.构建multipart请求
构建Multipart请求是占篇幅很大的一个功能,AFURLRequestSerialization里2/3的代码都是在做这个事。
 
A.Multipart协议介绍
Multipart是HTTP协议为web表单新增的上传文件的协议,协议文档是rfc1867,它基于HTTP的POST方法,数据同样是放在body上,跟普通POST方法的区别是数据不是key=value形式,key=value形式难以表示文件实体,为此Multipart协议添加了分隔符,有自己的格式结构,大致如下:
—AaB03x
content-disposition: form-data; name=“name"
bang
--AaB03x
content-disposition: form-data; name="pic"; filename=“content.txt"
Content-Type: text/plain
... contents of bang.txt ...
--AaB03x--
 
以上表示数据name=bang以及一个文件,content.txt是文件名,… contents of bang.txt …是文件实体内容。分隔符—AaB03x是可以自定义的,写在HTTP头部里:
 
Content-type: multipart/form-data, boundary=AaB03x
 
每一个部分都有自己的头部,表明这部分的数据类型以及其他一些参数,例如文件名,普通字段的key。最后一个分隔符会多加两横,表示数据已经结束:—AaB03x—。
 
B.实现
接下来说说怎样构造Multipart里的数据,最简单的方式就是直接拼数据,要发送一个文件,就直接把文件所有内容读取出来,再按上述协议加上头部和分隔符,拼接好数据后扔给NSURLRequest的body就可以发送了,很简单。但这样做是不可用的,因为文件可能很大,这样拼数据把整个文件读进内存,很可能把内存撑爆了。
 
第二种方法是不把文件读出来,不在内存拼,而是新建一个临时文件,在这个文件上拼接数据,再把文件地址扔给NSURLRequest的bodyStream,这样上传的时候是分片读取这个文件,不会撑爆内存,但这样每次上传都需要新建个临时文件,对这个临时文件的管理也挺麻烦的。
 
第三种方法是构建自己的数据结构,只保存要上传的文件地址,边上传边拼数据,上传是分片的,拼数据也是分片的,拼到文件实体部分时直接从原来的文件分片读取。这方法没上述两种的问题,只是实现起来也没上述两种简单,AFNetworking就是实现这第三种方法,而且还更进一步,除了文件,还可以添加多个其他不同类型的数据,包括NSData,和InputStream。
 
AFNetworking 里 multipart 请求的使用方式是这样:
  1. AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; 
  2. NSDictionary *parameters = @{@"foo": @"bar"}; 
  3. NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"]; 
  4. [manager POST:@"http://example.com/resources.json" parameters:parameters constructingBodyWithBlock:^(id formData) { 
  5.     [formData appendPartWithFileURL:filePath name:@"image" error:nil]; 
  6. } success:^(AFHTTPRequestOperation *operation, id responseObject) { 
  7.     NSLog(@"Success: %@", responseObject); 
  8. } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 
  9.     NSLog(@"Error: %@", error); 
  10. }]; 
这里通过constructingBodyWithBlock向使用者提供了一个AFStreamingMultipartFormData对象,调这个对象的几种append方法就可以添加不同类型的数据,包括FileURL/NSData/NSInputStream,AFStreamingMultipartFormData内部把这些append的数据转成不同类型的 AFHTTPBodyPart,添加到自定义的 AFMultipartBodyStream 里。最后把 AFMultipartBodyStream 赋给原来 NSMutableURLRequest的bodyStream。NSURLConnection 发送请求时会读取这个 bodyStream,在读取数据时会调用这个 bodyStream 的 -read:maxLength: 方法,AFMultipartBodyStream 重写了这个方法,不断读取之前 append进来的 AFHTTPBodyPart 数据直到读完。
 
AFHTTPBodyPart 封装了各部分数据的组装和读取,一个 AFHTTPBodyPart 就是一个数据块。实际上三种类型 (FileURL/NSData/NSInputStream) 的数据在 AFHTTPBodyPart 都转成 NSInputStream,读取数据时只需读这个 inputStream。inputStream 只保存了数据的实体,没有包括分隔符和头部,AFHTTPBodyPart 是边读取变拼接数据,用一个状态机确定现在数据读取到哪一部份,以及保存这个状态下已被读取的字节数,以此定位要读的数据位置,详见 AFHTTPBodyPart 的-read:maxLength:方法。
 
AFMultipartBodyStream封装了整个multipart数据的读取,主要是根据读取的位置确定现在要读哪一个AFHTTPBodyPart。AFStreamingMultipartFormData对外提供友好的append接口,并把构造好的AFMultipartBodyStream赋回给NSMutableURLRequest,关系大致如下图:
C.NSInputStream子类
NSURLRequest 的 setHTTPBodyStream 接受的是一个 NSInputStream* 参数,那我们要自定义inputStream的话,创建一个 NSInputStream 的子类传给它是不是就可以了?实际上不行,这样做后用NSURLRequest 发出请求会导致 crash,提示 [xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。
 
这是因为NSURLRequest实际上接受的不是 NSInputStream 对象,而是 CoreFoundation 的 CFReadStreamRef 对象,因为 CFReadStreamRef 和 NSInputStream 是 toll-free bridged,可以自由转换,但CFReadStreamRef 会用到 CFStreamScheduleWithRunLoop 这个方法,当它调用到这个方法时,object-c 的 toll-free bridging 机制会调用 object-c 对象 NSInputStream 的相应函数,这里就调用到了_scheduleInCFRunLoop:forMode:,若不实现这个方法就会crash。详见这篇文章。
 
3.源码注释
  1. AFURLRequestSerialization.m 








本篇说说安全相关的AFSecurityPolicy模块,AFSecurityPolicy用于验证HTTPS请求的证书,先来看看HTTPS的原理和证书相关的几个问题。

HTTPS

HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端 返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master  secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master  secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master  secret对传输数据进行加密解密。

这里说下一开始我比较费解的两个问题:

1.证书是怎样验证的?怎样保证中间人不能伪造证书?

首先要知道非对称加密算法的特点,非对称加密有一对公钥私钥,用公钥加密的数据只能通过对应的私钥解密,用私钥加密的数据只能通过对应的公钥解密。

我们来看最简单的情况:一个证书颁发机构(CA),颁发了一个证书A,服务器用这个证书建立https连接。客户端在信任列表里有这个CA机构的根证书。

首先CA机构颁发的证书A里包含有证书内容F,以及证书加密内容F1,加密内容F1就是用这个证书机构的私钥对内容F加密的结果。(这中间还有一次hash算法,略过。)

建立https连接时,服务端返回证书A给客户端,客户端的系统里的CA机构根证书有这个CA机构的公钥,用这个公钥对证书A的加密内容F1解密得 到F2,跟证书A里内容F对比,若相等就通过验证。整个流程大致是:F->CA私钥加密->F1->客户端CA公钥解密->F。 因为中间人不会有CA机构的私钥,客户端无法通过CA公钥解密,所以伪造的证书肯定无法通过验证。

2.什么是SSL Pinning?

可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书 是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架 构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。

为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确 实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私 钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。

为什么要用SSL  Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户 可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

AFSecurityPolicy

NSURLConnection已经封装了https连接的建立、数据的加密解密功能,我们直接使用NSURLConnection是可以访问 https网站的,但NSURLConnection并没有验证证书是否合法,无法避免中间人攻击。要做到真正安全通讯,需要我们手动去验证服务端返回的 证书,AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL  Pinning方式的验证。使用方法:

1
2
3
4
5
6
7
//把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
[AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;
[manager GET:@"https://example.com/" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];

AFSecurityPolicy分三种验证模式:

AFSSLPinningModeNone

这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。

AFSSLPinningModeCertificate

这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。

这里还没弄明白第一步的验证是怎么进行的,代码上跟去系统信任机构列表里验证一样调用了SecTrustEvaluate,只是这里的列表换成了客户端保存的那些证书列表。若要验证这个,是否应该把服务端证书的颁发机构根证书也放到客户端里?

AFSSLPinningModePublicKey

这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

整个AFSecurityPolicy就是实现这这几种验证方式,剩下的就是实现细节了,详见源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
// AFSecurity.m
//
// Copyright (c) 2013-2014 AFNetworking (http://afnetworking.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
  
#import "AFSecurityPolicy.h"
  
// Equivalent of macro in , without causing compiler warning:
// "'DebugAssert' is deprecated: first deprecated in OS X 10.8"
//这两个宏方法用于方便地处理调用各种证书方法过程中出现的错误,出现错误后用goto语句直接跳转到结束语
//关于为什么要用 __builtin_expect (x, 0) 而不直接用 x != 0,是为了CPU执行时的性能优化,见这里:
//http://stackoverflow.com/questions/7346929/why-do-we-use-builtin-expect-when-a-straightforward-way-is-to-use-if-else
#ifndef AF_Require
#define AF_Require(assertion, exceptionLabel)                \
do {                                                     \
if (__builtin_expect(!(assertion), 0)) {             \
goto exceptionLabel;                             \
}                                                    \
while (0)
#endif
  
#ifndef AF_Require_noErr
#define AF_Require_noErr(errorCode, exceptionLabel)          \
do {                                                     \
if (__builtin_expect(0 != (errorCode), 0)) {         \
goto exceptionLabel;                             \
}                                                    \
while (0)
#endif
  
#if !defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
static NSData * AFSecKeyGetData(SecKeyRef key) {
    CFDataRef data = NULL;
  
    AF_Require_noErr(SecItemExport(key, kSecFormatUnknown, kSecItemPemArmour, NULL, &data), _out);
  
    return (__bridge_transfer NSData *)data;
  
_out:
    if (data) {
        CFRelease(data);
    }
  
    return nil;
}
#endif
  
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
    return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
    return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}
  
//从证书里取public key
static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;
  
    //取证书SecCertificateRef -> 生成证书数组 -> 生成SecTrustRef -> 从SecTrustRef取PublicKey
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    AF_Require(allowedCertificate != NULL, _out);
  
    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
  
    policy = SecPolicyCreateBasicX509();
    AF_Require_noErr(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
    AF_Require_noErr(SecTrustEvaluate(allowedTrust, &result), _out);
  
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
  
_out:
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }
  
    if (policy) {
        CFRelease(policy);
    }
  
    if (tempCertificates) {
        CFRelease(tempCertificates);
    }
  
    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }
  
    return allowedPublicKey;
}
  
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    AF_Require_noErr(SecTrustEvaluate(serverTrust, &result), _out);
  
    //kSecTrustResultUnspecified:证书通过验证,但用户没有设置这些证书是否被信任
    //kSecTrustResultProceed:证书通过验证,用户有操作设置了证书被信任,例如在弹出的是否信任的alert框中选择always trust
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
  
_out:
    return isValid;
}
  
//取出服务端返回的所有证书
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
  
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }
  
    return [NSArray arrayWithArray:trustChain];
}
  
//取出服务端返回证书里所有的public key
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    for (CFIndex i = 0; i  生成证书数组 -> 生成SecTrustRef -> 从SecTrustRef取PublicKey
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
  
        SecCertificateRef someCertificates[] = {certificate};
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
  
        SecTrustRef trust;
        AF_Require_noErr(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
  
        SecTrustResultType result;
        AF_Require_noErr(SecTrustEvaluate(trust, &result), _out);
  
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
  
    _out:
        if (trust) {
            CFRelease(trust);
        }
  
        if (certificates) {
            CFRelease(certificates);
        }
  
        continue;
    }
    CFRelease(policy);
  
    return [NSArray arrayWithArray:trustChain];
}
  
#pragma mark -
  
@interface AFSecurityPolicy()
@property (readwrite, nonatomic, strong) NSArray *pinnedPublicKeys;
@end
  
@implementation AFSecurityPolicy
  
+ (NSArray *)defaultPinnedCertificates {
    //默认证书列表,遍历根目录下所有.cer文件
    static NSArray *_defaultPinnedCertificates = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSBundle *bundle = [NSBundle bundleForClass:[self class]];
        NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
  
        NSMutableArray *certificates = [NSMutableArray arrayWithCapacity:[paths count]];
        for (NSString *path in paths) {
            NSData *certificateData = [NSData dataWithContentsOfFile:path];
            [certificates addObject:certificateData];
        }
  
        _defaultPinnedCertificates = [[NSArray alloc] initWithArray:certificates];
    });
  
    return _defaultPinnedCertificates;
}
  
+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
  
    return securityPolicy;
}
  
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = pinningMode;
    securityPolicy.validatesDomainName = YES;
    [securityPolicy setPinnedCertificates:[self defaultPinnedCertificates]];
  
    return securityPolicy;
}
  
- (id)init {
    self = [super init];
    if (!self) {
        return nil;
    }
  
    self.validatesCertificateChain = YES;
  
    return self;
}
  
#pragma mark -
  
- (void)setPinnedCertificates:(NSArray *)pinnedCertificates {
    _pinnedCertificates = pinnedCertificates;
  
    if (self.pinnedCertificates) {
        //预先取出public key,用于AFSSLPinningModePublicKey方式的验证
        NSMutableArray *mutablePinnedPublicKeys = [NSMutableArray arrayWithCapacity:[self.pinnedCertificates count]];
        for (NSData *certificate in self.pinnedCertificates) {
            id publicKey = AFPublicKeyForCertificate(certificate);
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        self.pinnedPublicKeys = [NSArray arrayWithArray:mutablePinnedPublicKeys];
    else {
        self.pinnedPublicKeys = nil;
    }
}
  
#pragma mark -
  
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust {
    return [self evaluateServerTrust:serverTrust forDomain:nil];
}
  
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
  
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
  
    //向系统内置的根证书验证服务端返回的证书是否合法
    //若使用自签名证书,这里的验证是会不合法的,需要设allowInvalidCertificates = YES
    if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
  
    //取出服务端返回的证书
    NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
            //两种情况走到这里,
            //一是通过系统证书验证,返回认证成功
            //二是没通过验证,但allowInvalidCertificates = YES,也就是说完全不认证直接返回认证成功
            return YES;
  
            //验证整个证书
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            //在本地证书里验证服务端返回的证书的有效性
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }
  
            if (!self.validatesCertificateChain) {
                return YES;
            }
  
            //整个证书链都跟本地的证书匹配才给过
            NSUInteger trustedCertificateCount = 0;
            for (NSData *trustChainCertificate in serverCertificates) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    trustedCertificateCount++;
                }
            }
  
            return trustedCertificateCount == [serverCertificates count];
        }
  
            //只验证证书的public key
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
  
            //如果不用验证整个证书链,取第一个也就是真正会使用的那个证书验证就行
            if (!self.validatesCertificateChain && [publicKeys count] > 0) {
                publicKeys = @[[publicKeys firstObject]];
            }
  
            //在本地证书里搜索相等的public key,记录找到个数
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
  
            //验证整个证书链的情况:每个public key都在本地找到算验证通过
            //验证单个证书的情况:找到一个算验证通过
            return trustedPublicKeyCount > 0 && ((self.validatesCertificateChain && trustedPublicKeyCount == [serverCertificates count]) || (!self.validatesCertificateChain && trustedPublicKeyCount >= 1));
        }
    }
  
    return NO;
}
  
#pragma mark - NSKeyValueObserving
  
+ (NSSet *)keyPathsForValuesAffectingPinnedPublicKeys {
    return [NSSet setWithObject:@"pinnedCertificates"];
}
  
@end
0 0
原创粉丝点击