前言
作为iOS开发者,每每面试极容易问到的问题就是对HTTPS的了解,以及对AFN的了解,那么我们今天就结合这二者来好好研究一下这方面的内容。
网络协议
国际标准化组织(OSI)于1981年正式推荐了一个网络系统结构—-七层参考模型,OSI 参考模型将整个网络通信的功能划分为七个层次。它们由低到高分别是物理层(PH)、数据链路层(DL)、网络层(N)、传输层(T)、会话层(S)、表示层(P)、应用层(A)。每层完成一定的功能,每层都直接为其上层提供服务,并且所有层次都互相支持。第四层到第七层主要负责互操作性,而一层到三层则用于创造两个网络设备间的物理连接。
至于每一层的详细介绍这里就不贴了,感兴趣的同学可以自行搜索。说到这七层协议的主要原因是因为:TCP/IP协议在一定程度上参考了OSI的体系结构。OSI模型共有七层,但是这显然是有些复杂的,所以在TCP/IP协议中,它们被简化为了四个层次。
TCP/IP协议
按百度百科的介绍:TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。TCP/IP传输协议是严格来说是一个四层的体系结构,应用层、传输层、网络层和数据链路层都包含其中。
- 1、应用层:应用层是TCP/IP协议的第一层,是直接为应用进程提供服务的。
- 2、传输层:作为TCP/IP协议的第二层,传输层在整个TCP/IP协议中起到了中流砥柱的作用。且在运输层中,TCP和UDP也同样起到了中流砥柱的作用。
- 3、网络层:网络层在TCP/IP协议中的位于第三层。在TCP/IP协议中网络层可以进行网络连接的建立和终止以及IP地址的寻找等功能。
- 4、网络接口层:在TCP/IP协议中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。
TCP和UDP
前面说了那么多,我们知道了TCP和UDP是TCP/IP协议下面,传输协议层中的协议。也就是说TCP/IP->传输层->TCP和UDP。这俩才是高频出现的词汇嘛。
TCP
当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。 TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。TCP的报文格式见下图。
- URG:紧急标志。紧急标志为”1″表明该位有效。
- ACK:确认标志。表明确认编号栏有效。大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号(w+1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。
- PSH:推标志。该标志置位时,接收端不将该数据进行队列处理,而是尽可能快地将数据转由应用处理。在处理Telnet或rlogin等交互模式的连接时,该标志总是置位的。
- RST:复位标志。用于复位相应的TCP连接。
- SYN:同步标志。表明同步序列编号栏有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。
- FIN:结束标志。
TCP的工作方式
建立连接(三次握手)
- 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
- 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
- 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
连接终止(四次挥手)
- 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
- 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
- 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
- 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
- 这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
UDP
UDP协议与TCP协议一样用于处理数据包,在OSI模型中,两者都位于传输层,处于IP协议的上一层。UDP提供不可靠服务,具有TCP所没有的优势:
- UDP无连接,时间上不存在建立连接需要的时延。空间上,TCP需要在端系统中维护连接状态,需要一定的开销。此连接装入包括接收和发送缓存,拥塞控制参数和序号与确认号的参数。UCP不维护连接状态,也不跟踪这些参数,开销小。空间和时间上都具有优势。
- UDP没有拥塞控制,应用层能够更好的控制要发送的数据和发送时间,网络中的拥塞控制也不会影响主机的发送速率。某些实时应用要求以稳定的速度发送,能容 忍一些数据的丢失,但是不能允许有较大的时延(比如实时视频,直播等)
- UDP提供尽最大努力的交付,不保证可靠交付。所有维护传输可靠性的工作需要用户在应用层来完成。没有TCP的确认机制、重传机制。如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息。
- UDP是面向报文的,对应用层交下来的报文,添加首部后直接乡下交付为IP层,既不合并,也不拆分,保留这些报文的边界。对IP层交上来UDP用户数据报,在去除首部后就原封不动地交付给上层应用进程,报文不可分割,是UDP数据报处理的最小单位。
UDP的首部格式
UDP数据报分为首部和用户数据部分,整个UDP数据报作为IP数据报的数据部分封装在IP数据报中,UDP数据报文结构如图所示:
UDP首部有8个字节,由4个字段构成,每个字段都是两个字节。
- 1.源端口: 源端口号,需要对方回信时选用,不需要时全部置0
- 2.目的端口:目的端口号,在终点交付报文的时候需要用到。当传输层从IP层收到UDP数据报时,就根据首部中的目的端口,把UDP数据报通过相应的端口,上交给应用进程
- 3.长度:UDP的数据报的长度(包括首部和数据)其最小值为8(只有首部)
- 4.校验和:检测UDP数据报在传输中是否有错,有错则丢弃。该字段是可选的,当源主机不想计算校验和,则直接令该字段全为0
UDP校验
在计算校验和的时候,需要在UDP数据报之前增加12字节的伪首部,伪首部并不是UDP真正的首部。只是在计算校验和,临时添加在UDP数据报的前面,得到一个临时的UDP数据报。校验和就是按照这个临时的UDP数据报计算的。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验。
UDP校验和的计算方法和IP数据报首部校验和的计算方法相似,都使用二进制反码运算求和再取反,但不同的是:IP数据报的校验和之检验IP数据报和首部,但UDP的校验和是把首部和数据部分一起校验。
发送方,首先是把全零放入校验和字段并且添加伪首部,然后把UDP数据报看成是由许多16位的子串连接起来,若UDP数据报的数据部分不是偶数个字节,则要在数据部分末尾增加一个全零字节(此字节不发送),接下来就按照二进制反码计算出这些16位字的和。将此和的二进制反码写入校验和字段。在接收方,把收到得UDP数据报加上伪首部(如果不为偶数个字节,还需要补上全零字节)后,按二进制反码计算出这些16位字的和。当无差错时其结果全为1。否则就表明有差错出现,接收方应该丢弃这个UDP数据报。
协议对比
- UDP和TCP协议的主要区别是两者在如何实现信息的可靠传递方面不同。TCP协议中包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其它信息,否则将一直等待直到收到确认信息为止。与TCP不同,UDP协议并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据包的丢失,协议本身并不能做出任何检测或提示。因此,通常人们把UDP协议称为不可靠的传输协议。
- TCP 是面向连接的传输控制协议,而UDP 提供了无连接的数据报服务;TCP 具有高可靠性,确保传输数据的正确性,不出现丢失或乱序;UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序,应用程序需要负责传输可靠性方面的所有工作;UDP 具有较好的实时性,工作效率较 TCP 协议高;UDP 段结构比 TCP 的段结构简单,因此网络开销也小。TCP 协议可以保证接收端毫无差错地接收到发送端发出的字节流,为应用程序提供可靠的通信服务。对可靠性要求高的通信系统往往使用 TCP 传输数据。
HTTP和HTTPS
什么是HTTP?
超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
具有以下特点:
- 无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作
- 无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
- 基于请求和响应:基本的特性,由客户端发起请求,服务端响应
- 简单快速、灵活
- 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性
什么是HTTPS?
HTTPS是一种通过计算机网络进行安全通信的传输协议,经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性。是身披SSL外壳的HTTP。
既然是经过了SSL加密了的,当然具有以下特点:
- 内容加密:采用混合加密技术,中间者无法直接查看明文内容
- 验证身份:通过证书认证客户端访问的是自己的服务器
- 保护数据完整性:防止传输的内容被中间人冒充或者篡改
HTTPS实现原理
SSL建立连接过程:
- client向server发送请求
https://baidu.com
,然后连接到server的443端口,发送的信息主要是随机值1和客户端支持的加密算法。 - server接收到信息之后给予client响应握手信息,包括随机值2和匹配好的协商加密算法,这个加密算法一定是client发送给server加密算法的子集。
- 随即server给client发送第二个响应报文是数字证书。服务端必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面,这套证书其实就是一对公钥和私钥。传送证书,这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间、服务端的公钥,第三方证书认证机构(CA)的签名,服务端的域名信息等内容。
- 客户端解析证书,这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值(预主秘钥)。
- 客户端认证证书通过之后,接下来是通过随机值1、随机值2和预主秘钥组装会话秘钥。然后通过证书的公钥加密会话秘钥。
- 传送加密信息,这部分传送的是用证书加密后的会话秘钥,目的就是让服务端使用秘钥解密得到随机值1、随机值2和预主秘钥。
- 服务端解密得到随机值1、随机值2和预主秘钥,然后组装会话秘钥,跟客户端会话秘钥相同。
- 客户端通过会话秘钥加密一条消息发送给服务端,主要验证服务端是否正常接受客户端加密的消息。
- 同样服务端也会通过会话秘钥加密一条消息回传给客户端,如果客户端能够正常接受的话表明SSL层连接建立完成了。
其中验证证书安全性过程:
- 当客户端收到这个证书之后,使用本地配置的权威机构的公钥对证书进行解密得到服务端的公钥和证书的数字签名,数字签名经过CA公钥解密得到证书信息摘要。
- 然后证书签名的方法计算一下当前证书的信息摘要,与收到的信息摘要作对比,如果一样,表示证书一定是服务器下发的,没有被中间人篡改过。因为中间人虽然有权威机构的公钥,能够解析证书内容并篡改,但是篡改完成之后中间人需要将证书重新加密,但是中间人没有权威机构的私钥,无法加密,强行加密只会导致客户端无法解密,如果中间人强行乱修改证书,就会导致证书内容和证书签名不匹配。
总得来说,就是使用非对称加密建立链接,然后使用对称加密进行数据传输。既然我们知道了HTTPS的工作流程,那么我们就根据这个思路去看一看AFNetworking是什么实现的吧。
AFNetworking
AFNetworking的结构
从这张图可以看出:除去Support Files,可以看到AFN分为如下5个功能模块:
- 网络通信模块(最核心)(AFURLSessionManager、AFHTTPSessionManager)
- 网络状态监听模块(Reachability)
- 网络通信安全策略模块(Security)
- 网络通信信息序列化/反序列化模块(Serialization)
- 对于iOS UIkit库的拓展(UIKit)
现在我们主要来谈论一下最核心的模块网络通信模块。这里面有2个类,分别是AFURLSessionManager和AFHTTPSessionManager。AFHTTPSessionManager是继承AFURLSessionManager的,相当于对AFURLSessionManager的再次封装。
1.AFURLSessionManager
基本介绍
AFURLSessionManager遵守NSSecureCoding,NSCopying两个协议,以及遵守NSURLSessionDelegate,NSURLSessionTaskDelegate,NSURLSessionDataDelegate,NSURLSessionDownloadDelegate四个代理。在AFURLSessionManager中实现协议里的方法,用来处理网络请求中不同的情况:例如:暂停,取消,数据保存,更新数据进度条。其所包含的属性和解释如下:
核心方法和执行流程
- AFHTTPSessionManager的初始化
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}
- (instancetype)init {
return [self initWithBaseURL:nil];
}
- (instancetype)initWithBaseURL:(NSURL *)url {
return [self initWithBaseURL:url sessionConfiguration:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
return [self initWithBaseURL:nil sessionConfiguration:configuration];
}
复制代码
最后都会落在这里
/*
1.调用父类的方法
2.给url添加“/”
3.给requestSerializer、responseSerializer设置默认值
*/
- (instancetype)initWithBaseURL:(NSURL *)url
sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
//调用父类初始化方法
self = [super initWithSessionConfiguration:configuration];
if (!self) {
return nil;
}
// Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
/*
为了确保NSURL +URLWithString:relativeToURL: works可以正确执行,在baseurlpath的最后添加‘/’
*/
//url有值且没有‘/’,那么在url的末尾添加‘/’
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
self.baseURL = url;
//给requestSerializer、responseSerializer设置默认值
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
return self;
}
复制代码
调用了父类的初始化方法,于是来到这里:
/*
1.初始化一个session
2.给manager的属性设置初始值
*/
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
self = [super init];
if (!self) {
return nil;
}
//设置默认的configuration,配置我们的session
if (!configuration) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
//持有configuration
self.sessionConfiguration = configuration;
//设置为delegate的操作队列并发的线程数量1,也就是串行队列
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
/*
-如果完成后需要做复杂(耗时)的处理,可以选择异步队列
-如果完成后直接更新UI,可以选择主队列
[NSOperationQueue mainQueue]
*/
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
//默认为json解析
self.responseSerializer = [AFJSONResponseSerializer serializer];
//设置默认证书 无条件信任证书https认证
self.securityPolicy = [AFSecurityPolicy defaultPolicy];
#if !TARGET_OS_WATCH
//网络状态监听
self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endif
//delegate= value taskid = key
self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
//使用NSLock确保线程安全
self.lock = [[NSLock alloc] init];
self.lock.name = AFURLSessionManagerLockName;
//异步的获取当前session的所有未完成的task。其实讲道理来说在初始化中调用这个方法应该里面一个task都不会有
//后台任务重新回来初始化session,可能就会有先前的任务
//https://github.com/AFNetworking/AFNetworking/issues/3499
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionDataTask *task in dataTasks) {
[self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
}
for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
[self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
}
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
[self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
}
}];
return self;
}
复制代码
至此,session初始化完成。
- 接下来我们看看请求是如何发起的
以GET方法为例:
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))downloadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
//返回一个task,然后开始网络请求
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
URLString:URLString
parameters:parameters
uploadProgress:nil
downloadProgress:downloadProgress
success:success
failure:failure];
//开始网络请求
[dataTask resume];
return dataTask;
}
复制代码
创建task的实现:
//1.生成request,2.通过request成成task
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
/*
1.先调用AFHTTPRequestSerializer的requestWithMethod函数构建request
2.处理request构建产生的错误 – serializationError
//relativeToURL表示将URLString拼接到baseURL后面
*/
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
//http://fuckingclangwarnings.com/#semantic
//xcode忽略编译器的警告,diagnostic:诊断的
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//completionQueue不存在返回dispatch_get_main_queue
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
//此时的request已经将参数拼接在url后面
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
复制代码
这里有两个东西需要生成,一个是request,然后通过request成成task。
生成request在AFHTTPRequestSerializer中:
/**
使用指定的HTTP method和URLString来构建一个NSMutableURLRequest对象实例
如果method是GET、HEAD、DELETE,那parameter将会被用来构建一个基于url编码的查询字符串(query url)
,并且这个字符串会直接加到request的url后面。对于其他的Method,比如POST/PUT,它们会根
据parameterEncoding属性进行编码,而后加到request的http body上。
@param method request的HTTP methodt,比如 `GET`, `POST`, `PUT`, or `DELETE`. 该参数不能为空
@param URLString 用来创建request的URL
@param parameters 既可以对method为GET的request设置一个查询字符串(query string),也可以设置到request的HTTP body上
@param error 构建request时发生的错误
@return 一个NSMutableURLRequest的对象
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
//断言如果nil,直接打印出来
NSParameterAssert(method);
NSParameterAssert(URLString);
//我们传进来的是一个字符串,在这里它帮你转成url
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
//设置请求方式(get、post、put。。。)
mutableRequest.HTTPMethod = method;
//将request的各种属性遍历,给NSMutableURLRequest自带的属性赋值
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
//给设置过得的属性,添加到request(如:timeout)
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
//通过kvc动态的给mutableRequest添加value
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
//将传入的参数进行编码,拼接到url后并返回 coount=5&start=1
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
NSLog(@"request'''''''''%@",mutableRequest);
return mutableRequest;
}
复制代码
我们知道,一个请求就是一个request,所以我们的重点要看看request是如何生成的。顺着实现继续往下看,我们发现了一个C函数AFHTTPRequestSerializerObservedKeyPaths()
,返回了一个数组,里面包含了NSMutableURLRequest的各种属性。他的实现如下:
//返回一个方法名的数组
// 定义了一个static的方法,表示该方法只能在本文件中使用
/*
1. 是否允许使用设备的蜂窝移动网络来创建request,默认为允许:
2.创建的request所使用的缓存策略,默认使用`NSURLRequestUseProtocolCachePolicy`,该策略表示
如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断
下一步操作,如: Cache-Control字段为must-revalidata, 则 询问服务端该数据是否有更新,无更新话
直接返回给用户缓存数据,若已更新,则请求服务端.
3. 如果设置HTTPShouldHandleCookies为YES,就处理存储在NSHTTPCookieStore中的cookies
HTTPShouldHandleCookies表示是否应该给request设置cookie并随request一起发送出去
4.HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送。
如果为YES表示可以,NO表示必须等receiver收到先前的回复才能发送下个信息
5.设定request的network service类型. 默认是`NSURLNetworkServiceTypeDefault`.
这个network service是为了告诉系统网络层这个request使用的目的
比如NSURLNetworkServiceTypeVoIP表示的就这个request是用来请求网际协议通话技术(Voice over IP)。
系统能根据提供的信息来优化网络处理,从而优化电池寿命,网络性能等等,客户端基本不使用
6.超时机制,默认60秒
*/
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_AFHTTPRequestSerializerObservedKeyPaths =
@[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
});
return _AFHTTPRequestSerializerObservedKeyPaths;
}
复制代码
在AFURLRequestSerialization里面的init初始化mutableObservedChangedKeyPaths这个NSSet方法,并添加观察者利用kVO模式进行监听:
self.mutableObservedChangedKeyPaths = [NSMutableSet set];
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
//自己给自己的方法添加观察者
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
}
}
复制代码
KVO触发:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(__unused id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == AFHTTPRequestSerializerObserverContext) {
if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
[self.mutableObservedChangedKeyPaths removeObject:keyPath];
} else {
[self.mutableObservedChangedKeyPaths addObject:keyPath];
}
}
}
复制代码
接下来是将传入的parameters进行编码,并添加到request:
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
//给mutableRequest.headfiled赋值
/*
1.请求行(状态行):get,url,http协议1.1
2.请求头:conttent-type,accept-language
3.请求体:get/post get参数拼接在url后面 post数据放在body
*/
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
//将我们传入的字典转成字符串
NSString *query = nil;
if (parameters) {
//自定义的解析方式
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
//默认解析方式,dic- count=5&start=1
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
//将parameters传入这个c函数
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
//count=5&start=1
NSLog(@"query:%@",query);
//最后判断该request中是否包含了GET、HEAD、DELETE(都包含在HTTPMethodsEncodingParametersInURI)。因为这几个method的query是拼接到url后面的。而POST、PUT是把query拼接到http body中的。
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query && query.length > 0) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
//函数会判断request的Content-Type是否设置了,如果没有,就默认设置为application/x-www-form-urlencoded
//application/x-www-form-urlencoded是常用的表单发包方式,普通的表单提交,或者js发包,默认都是通过这种方式
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
//设置请求体
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
复制代码
其实解码里面最重要的就是AFQueryStringPairsFromKeyAndValue()方法,通过判断value的类型(array,dic,set),不断递归自己,最后返回一数组:
FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary);
FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value);
//?count=5&start=1
//这个方法,是遍历数组中的AFQueryStringPair,然后以&符号拼接。AFQueryStringPair是一个数据处理类,只有两个属性:field和value;一个方法:-URLEncodedStringValue。它的作用就是上面我们说的,以key=value的形式,用URL Encode编码,拼接成字符串
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
NSMutableArray *mutablePairs = [NSMutableArray array];//一对
//把参数传给AFQueryStringPairsFromDictionary,AFQueryStringPair数据处理类
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
//百分号编码
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
//拆分数组返回参数字符串
return [mutablePairs componentsJoinedByString:@"&"];
}
//过渡
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}
//使用了递归
/*
接着会对value的类型进行判断,有NSDictionary、NSArray、NSSet类型。不过有人就会问了,在AFQueryStringPairsFromDictionary中给AFQueryStringPairsFromKeyAndValue函数传入的value不是NSDictionary嘛?还要判断那么多类型干啥?对,问得很好,这就是AFQueryStringPairsFromKeyAndValue的核心----递归调用并解析,你不能保证NSDictionary的value中存放的是一个NSArray、NSSet。
*/
//第二次,key=count valus=5
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {//key=count value = 5
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
/*
根据需要排列的对象的description来进行升序排列,并且selector使用的是compare:
因为对象的description返回的是NSString,所以此处compare:使用的是NSString的compare函数
*/
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];
//判断vaLue是什么类型的,然后去递归调用自己,直到解析的是除了array dic set以外的元素,然后把得到的参数数组返回。
if ([value isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = value;
// Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
/*
allkeys:count/start
*/
for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
id nestedValue = dictionary[nestedKey];//nestedkey=count nestedvalue=5
if (nestedValue) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}
}
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray *array = value;
for (id nestedValue in array) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}
} else if ([value isKindOfClass:[NSSet class]]) {
NSSet *set = value;
for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
}
} else {//既然是递归,那么就要有结束递归的情况,比如解析到最后,对应value是一个NSString,那么就得调用函数中最后的else语句,
//AFQueryStringPair数据处理类,mutableQueryStringComponents中的元素类型是AFQueryStringPair
[mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
}
return mutableQueryStringComponents;
}
复制代码
其中AFQueryStringPair这个对象的实现:
/*
百分号编码count=5*# ASCII uinicode
count=5
*/
- (NSString *)URLEncodedStringValue {
if (!self.value || [self.value isEqual:[NSNull null]]) {
return AFPercentEscapedStringFromString([self.field description]);
} else {
return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
}
}
复制代码
如果是GET/HEAD/DELETE,是把参数拼接到url后面的,而POST/PUT是把query拼接到http body中,至此生成了一个request。
最后在当前类中创建task并返回:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler {
//为了解决iOS8一下的一个bug,调用一个串行队列来创建dataTask
__block NSURLSessionDataTask *dataTask = nil;
url_session_manager_create_task_safely(^{
//原生的方法
//使用session来创建一个NSURLSessionDataTask对象
dataTask = [self.session dataTaskWithRequest:request];
});
//为什么要给task添加代理呢?
[self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];
return dataTask;
}
复制代码
于是一整个请求任务就创建完成啦!
既然我们知道了发起请求的过程,那么按照HTTPS的原理,我还想知道在AFN中是如何设置HTTPS的,于是我们继续。
2.AFURLRequestSerialization请求的序列化
基本介绍:
- 实现了AFURLRequestSerialization协议,协议中只有一个方法
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
用于返回一个原始request的copy对象,将参数根据指定的编码格式进行处理。 - AFHTTPRequestSerializer作为请求序列化的一个根类,作为AFN的默认设置。AFJSONRequestSerializer、AFPropertyListRequestSerializer作为子类都继承自AFHTTPRequestSerializer。
- 头文件中还存在AFMultipartFormData协议,主要用于多部分表单的处理,之后将以表单形式POST请求为例,来分析其中的工作流程。
- AFURLRequestSerialization协议,继承自<NSObject, NSSecureCoding, NSCopying>三个协议。其中NSSecureCoding协议,主要用于在解码时要同时指定key和要解码的对象的类,如果要求的类和从文件中解码出的对象的类不匹配,NSCoder则会抛出异常并通知数据已经被篡改。NSCopying协议是为了能够让当前类支持拷贝功能。
执行流程:
以POST请求为例,提交的数据都是放到请求体body中,但并未规定编码方式,那么就需要设置Content-Type告知后台服务数据的格式。
通过声明一个AFMultipartFormData类型的formData来构建用于multipartForm请求体request,实现如下:
//构建一个multipartForm的request。并且通过`AFMultipartFormData`类型的formData来构建请求体
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
//method不能是get、head
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
// 使用initWithURLRequest:stringEncoding:来初始化一个AFStreamingMultipartFormData变量
// 主要是为了构建bodyStream
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
//通常nslog打印会调用description,打印出来的是地址,但是可以重写description,来实现打印出我们想要的类型
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
// bodyStream构造最主要的部分就在这了(虽然后面requestByFinalizingMultipartFormData函数还会稍微处理一下)
// 根据data和name构建Request的header和body,后面详解
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
// 参考上面的例子,其实还是往formData中添加数据
if (block) {
block(formData);
}
// 做最终的处理,比如设置一下MultipartRequest的bodyStream或者其特有的content-type等等,后面也会详解
return [formData requestByFinalizingMultipartFormData];
}
复制代码
创建完成AFStreamingMultipartFormData对象后,接下来的操作与基本POST请求相同,遍历parameters参数字典,将其转换成NSData并拼接进之前的AFStreamingMultipartFormData对象中。而构造bodyStream最主要的实现,在这[formData appendPartWithFormData:data name:[pair.field description]]
,根据data和name来构建request的header与body。
在方法实现中,拼接成符合表单传输的格式,并添加至实例变量bodyStream中,也就是对应的body数据。
接下来的,执行block(formData)代码块,就可以在代码实现的block中将图片添加至formData。
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
添加本地图片实现方法:
NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"upload" ofType:@"png"];
[formData appendPartWithFileURL:[NSURL fileURLWithPath:imgPath] name:@"(后台指定的key名)" error:nil];
复制代码
添加图片至body数据流中的实现方法,首先利用文件扩展名和C函数获取UTI统一类型标志符,再根据UTI获取contentType。
static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
if (!contentType) {
return @"application/octet-stream";
} else {
return contentType;
}
复制代码
接着,检查fileURL是否合法以及文件是否存在。若文件存在,创建一个AFHTTPBodyPart对象,拼接成符合表单数据结构的字典并放入该对象的header中,完成后将AFHTTPBodyPart对象添加至body数据对象bodyStream。
走到这一步,表单中的参数拼接已经完成。但一个完整的表单格式的请求参数,还缺少基本的信息,而保证这些信息的完整性,最后由[formData requestByFinalizingMultipartFormData]
方法实现,在表单的首尾添加分隔符、设置request的bodyStream为self.bodyStream(而非setBody方法)、设置Content-Type、设置Content-Length这四步操作。
针对表单形式的POST请求,request的初始化已经完成。之后task任务创建与处理,与普通的POST请求无异。AFN框架在表单形式的POST请求中,帮我们做了添加分隔符、并将所有的传参data拼接在一起,作为一个完整的请求数据流发送给服务器等一系列工作。
通过以上流程的了解,可以总结出AFURLRequestSerialization类的作用:
- 使用KVO以及KVC来动态监听并修改request属性;
- 设置request的请求header;
- 生成请求参数查询字符串;
- 支持表单结构数据以数据流拼接分片上传。
3.AFSecurityPolicy
基本介绍
苹果已经封装了HTTPS连接的建立、数据的加密解密功能,我们直接可以访问https网站的,但苹果并没有验证证书是否合法,无法避免中间人攻击。要做到真正安全通讯,需要我们手动去验证服务端返回的证书。AFNetwork中的AFSecurityPolicy模块主要是用来验证HTTPS请求时证书是否正确。AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证。
使用方法:
把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件。
// .crt --->.cer
// 证书 ---> 公钥 ---> 随机数 加密
// 项目本地导入 ---> 谁安装谁就能获取到这个证书
// 证书:proy.
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"https" ofType:@"cer"];
NSData *data = [NSData dataWithContentsOfFile:cerPath];
NSSet *cerSet = [NSSet setWithObject:data];
AFSecurityPolicy *security = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:cerSet];
// 默认配置
[AFSecurityPolicy defaultPolicy];
security.allowInvalidCertificates = YES;
security.validatesDomainName = NO;
NSString *urlstr = @"https://xxx.xxx.34.197:9000/users/abc";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// 不安全 -- 授权
manager.securityPolicy = security;
[manager GET:urlstr parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"success--%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"fail--%@",error);
}];
复制代码
属性介绍
//AFSecurityPolicy.h
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
//不使用固定证书(本地)验证服务器。直接从客户端系统中的受信任颁发机构 CA 列表中去验证
AFSSLPinningModeNone,
// 代表会对服务器返回的证书中的PublicKey进行验证,通过则通过,否则不通过
AFSSLPinningModePublicKey,
// 代表会对服务器返回的证书同本地证书全部进行校验,通过则通过,否则不通过
AFSSLPinningModeCertificate,
};
@interface AFSecurityPolicy : NSObject <NSSecureCoding, NSCopying>
// 返回SSL Pinning的类型。默认的是AFSSLPinningModeNone。
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
这个属性保存着所有的可用做校验的证书的集合。AFNetworking默认会搜索工程中所有.cer的证书文件。
如果想指定某些证书,可使用certificatesInBundle在目标路径下加载证书,
然后调用policyWithPinningMode:withPinnedCertificates创建一个本类对象。
注意: 只要在证书集合中任何一个校验通过,evaluateServerTrust:forDomain: 就会返回true,即通过校验。
*/
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
// 使用允许无效或过期的证书,默认是不允许。
@property (nonatomic, assign) BOOL allowInvalidCertificates;
// 是否验证证书中的域名domain
@property (nonatomic, assign) BOOL validatesDomainName;
// 返回指定bundle中的证书。如果使用AFNetworking的证书验证 ,就必须实现此方法,
并且使用policyWithPinningMode:withPinnedCertificates 方法来创建实例对象。
+ (NSSet <NSData *> *)certificatesInBundle:(NSBundle *)bundle;
/**
默认的实例对象,默认的认证设置为:
1. 不允许无效或过期的证书
2. 验证domain名称
3. 不对证书和公钥进行验证
*/
+ (instancetype)defaultPolicy;
// 创建默认实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;
// 根据指定的证书和pinningMode来创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet <NSData *> *)pinnedCertificates;
// 校验的关键方法 sessionmanager会调用
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(nullable NSString *)domain;
@end
复制代码
初始化
在使用 AFSecurityPolicy 验证服务端是否受到信任之前,要对其进行初始化,使用初始化方法时,主要目的是设置验证服务器是否受信任的方式。
// 根据指定的SSL验证模式创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
// 获得APP下的所有证书 [self defaultPinnedCertificates]
return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
}
// 根据SSL验证模式和指定的证书集合创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = pinningMode;
// 设置证书集合 如果是默认的 已经通过[self defaultPinnedCertificates]得到了
[securityPolicy setPinnedCertificates:pinnedCertificates];
// 公钥的取出
return securityPolicy;
}
复制代码
在调用 pinnedCertificates 的 setter 方法时,会从全部的证书中取出公钥保存到 pinnedPublicKeys 属性中。
// 此函数设置securityPolicy中的pinnedCertificates属性
// 注意还将对应的self.pinnedPublicKeys属性也设置了,该属性表示的是对应证书的公钥(与pinnedCertificates中的证书是一一对应的)
- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
_pinnedCertificates = pinnedCertificates;
//获取对应公钥集合
if (self.pinnedCertificates) {
//创建公钥集合
NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
//从证书中拿到公钥。
for (NSData *certificate in self.pinnedCertificates) {
// 取出合法的公钥
// 传输 -- session -- 验证当前所有的信息
id publicKey = AFPublicKeyForCertificate(certificate);
if (!publicKey) {
continue;
}
[mutablePinnedPublicKeys addObject:publicKey];
}
self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
} else {
self.pinnedPublicKeys = nil;
}
}
复制代码
调用了 AFPublicKeyForCertificate 对证书进行操作,返回一个公钥。
操作 SecTrustRef
static id AFPublicKeyForCertificate(NSData *certificate)
在该函数是返回单个证书的公钥(所以传入的参数是一个证书),函数代码如下(含注释):
static id AFPublicKeyForCertificate(NSData *certificate) {
id allowedPublicKey = nil;
SecCertificateRef allowedCertificate;
SecCertificateRef allowedCertificates[1];
CFArrayRef tempCertificates = nil;
SecPolicyRef policy = nil;
SecTrustRef allowedTrust = nil;
SecTrustResultType result;
// 1. 根据二进制的certificate生成SecCertificateRef类型的证书
// NSData *certificate 通过CoreFoundation (__bridge CFDataRef)转换成 CFDataRef
allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
// 2.如果allowedCertificate为空,则执行标记_out后边的代码
__Require_Quiet(allowedCertificate != NULL, _out);
allowedCertificates[0] = allowedCertificate;
tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
// 新建policy为X.509
policy = SecPolicyCreateBasicX509();
//3. 创建SecTrustRef对象,如果出错就跳到_out标记处
__Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
// 4.校验证书是否可信任的过程,这个不是异步的。
__Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
// 5.在SecTrustRef对象中取出公钥
// 公钥是授信的,下面一行代码就是为了证明这个公钥是合法的
allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
// _out 用途 释放各种 C 语言指针
_out:
if (allowedTrust) {
CFRelease(allowedTrust);
}
if (policy) {
CFRelease(policy);
}
if (tempCertificates) {
CFRelease(tempCertificates);
}
if (allowedCertificate) {
CFRelease(allowedCertificate);
}
/*
① NSData *certificate -> CFDataRef -> (SecCertificateCreateWithData) -> SecCertificateRef allowedCertificate
②判断SecCertificateRef allowedCertificate 是不是空,如果为空,直接跳转到后边的代码 即:_out
③SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust) -> 生成SecTrustRef allowedTrust
④SecTrustEvaluate(allowedTrust, &result) 校验证书
⑤(__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust) -> 得到公钥id allowedPublicKey
*/
return allowedPublicKey;
}
复制代码
验证服务端是否受信
核心代码:- [AFSecurityPolicy evaluateServerTrust:forDomain:]
SecTrustRef:
- 其实就是一个容器,装了服务器端需要验证的证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端的证书去匹配验证的。
- 每一个SecTrustRef对象包含多个SecCertificateRef 和 SecPolicyRef。其中 SecCertificateRef 可以使用 DER 进行表示。
domain:服务器域名,用于域名验证。
- (BOOL)evaluateServerTrust:( SecTrustRef )serverTrust
forDomain:( NSString *)domain
{
// 当使用自建证书验证域名时,需要使用AFSSLPinningModePublicKey或者AFSSLPinningModeCertificate进行验证
if (domain && self . allowInvalidCertificates && self . validatesDomainName && ( self . SSLPinningMode == AFSSLPinningModeNone || [ self . pinnedCertificates count ] == 0 )) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog ( @"In order to validate a domain name for self signed certificates, you MUST use pinning." );
return NO ;
}
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);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
//SSLPinningMode为AFSSLPinningModeNone时,allowInvalidCertificates为YES,则代表服务器任何证书都能验证通过;
如果它为NO,则需要判断此服务器证书是否是系统信任的证书
return self . allowInvalidCertificates || AFServerTrustIsValid (serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 如果服务器证书不是系统信任证书,且不允许不信任的证书通过验证则返回NO
return NO ;
}
switch ( self . SSLPinningMode ) {
case AFSSLPinningModeNone :
default :
return NO ;
case AFSSLPinningModeCertificate: {
//AFSSLPinningModeCertificate是直接将本地的证书设置为信任的根证书,然后来进行判断,并且比较本地证书的内容和
服务器证书内容是否相同,如果有一个相同则返回YES
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 ;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust (serverTrust);
// 判断本地证书和服务器证书的内容是否相同
for ( NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator ]) {
if ([ self . pinnedCertificates containsObject :trustChainCertificate]) {
return YES ;
}
}
return NO ;
}
case AFSSLPinningModePublicKey : {
NSUInteger trustedPublicKeyCount = 0 ;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust (serverTrust);
//AFSSLPinningModePublicKey是通过比较证书当中公钥(PublicKey)部分来进行验证,
通过SecTrustCopyPublicKey方法获取本地证书和服务器证书,然后进行比较,如果有一个相同,则通过验证
for ( id trustChainPublicKey in publicKeys) {
for ( id pinnedPublicKey in self . pinnedPublicKeys ) {
if ( AFSecKeyIsEqualToKey (( __bridge SecKeyRef )trustChainPublicKey, ( __bridge SecKeyRef )pinnedPublicKey)) {
trustedPublicKeyCount += 1 ;
}
}
}
return trustedPublicKeyCount > 0 ;
}
}
return NO ;
}
复制代码
执行流程:
- 不能隐式地信任自己签发的证书:如果有服务器域名、设置了允许信任无效或者过期证书(自签名证书)、需要验证域名、没有提供证书或者不验证证书,返回NO。后两者和allowInvalidCertificates为真的设置矛盾,说明这次验证是不安全的。
- 设置 policy
- 验证证书是否有效
- 根据 SSLPinningMode 对服务端进行验证
核心代码解释说明:
- 根据severTrust和domain来检查服务器端发来的证书是否可信
- 其中SecTrustRef是一个CoreFoundation类型,用于对服务器端传来的X.509证书评估的
- 我们已经知道,数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包含的持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。
与 AFURLSessionManager 协作
//HTTPS认证
/*
该代理方法会在下面两种情况调用:
当服务器端要求客户端提供证书时或者进行NTLM认证(Windows NT LAN Manager,微软提出的WindowsNT挑战/响应验证机制)时,此方法允许你的app提供正确的挑战证书。
当某个session使用SSL/TLS协议,第一次和服务器端建立连接的时候,服务器会发送给iOS客户端一个证书,此方法允许你的app验证服务期端的证书链(certificate keychain)
注:如果你没有实现该方法,该session会调用其NSURLSessionTaskDelegate的代理方法URLSession:task:didReceiveChallenge:completionHandler: 。
这里,我把官方文档对这个方法的描述翻译了一下。
总结一下,这个方法其实就是做https认证的。看看上面的注释,大概能看明白这个方法做认证的步骤,我们还是如果有自定义的做认证的Block,则调用我们自定义的,否则去执行默认的认证步骤,最后调用完成认证
服务端发起的一个验证挑战,客户端需要根据挑战的类型提供相应的挑战凭证。当然,挑战凭证不一定都是进行HTTPS证书的信任,也可能是需要客户端提供用户密码或者提供双向验证时的客户端证书。当这个挑战凭证被验证通过时,请求便可以继续顺利进行
*/
//收到服务端的challenge,例如https需要验证证书等 ats开启
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//挑战处理类型为 默认
/*
NSURLSessionAuthChallengeUseCredential:使用指定的证书
NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
NSURLSessionAuthChallengeRejectProtectionSpace:拒绝此挑战,并尝试下一个验证保护空间;忽略证书参数
*/
//挑战处理类型为默认
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;//证书
// 自定义方法,用来如何应对服务器端的认证挑战
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
// 1.判断接收服务器挑战的方法是否是信任证书
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//只需要验证服务端证书是否安全(即https的单向认证,这是AF默认处理的认证方式,其他的认证方式,只能由我们自定义Block的实现
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 2.信任评估通过,就从受保护空间里面拿出证书,回调给服务器,告诉服务,我信任你,你给我发送数据吧.
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 确定挑战的方式
if (credential) {
//证书挑战
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
//默认挑战
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
//取消挑战
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
//默认挑战方式
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
//完成挑战
// 3.将信任凭证发送给服务端
if (completionHandler) {
completionHandler(disposition, credential);
}
}
复制代码
总结:
经过以上篇幅的介绍
- 我们从网络七层协议模型OSI入手,知道了TCP/IP协议是一个协议簇,将七层模型简化成为了四层,而TCP和UDP都在其中的传输层中。
- 还知道了TCP的三次握手和四次挥手,也知道了UDP不需要建立链接就能实现传输。所以二者一个可靠且稳定,但是要维护连接状态开销较大,一个实时性高,但是会有丢包,错乱,重复等问题,可以根据实际情况选择合适的策略。
- 也知道了HTTP是超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,当然也知道了HTTPS实际上是在HTTP身上套了一层SSL加密的壳。同时也知道了HTTPS建立链接的过程。
- 最后我们分析了AFN的源码,知道了各个模块的功能,知道了一个请求发起的流程,知道了序列化和安全策略两个类是怎么实现的。
其中的重点是HTTPS建立链接的过程和AFN的源码分析。
完