对AFNetworking进行解耦
AFNetworking解耦后可以分为以下几个模块:
1. NSURLSession:主要的一个基于NSURLSession的管理模块;
2. Reachability:网络监测模块;
3. Security:Https验证模块;
4. Serialization:序列化模块,包含了请求和响应的序列化;
5. UIKit:包含了一些UI的扩展,方便调用。
NSURLSession 和 Reachability 还有 Serialization 、UIKit 这些模块就不做过多的解析了,重点了解一下 Security中Https的认证。
Security
-
Https通讯流程
详见以前的一篇文章:[https通讯流程],(https://www.jianshu.com/p/dc0113819344),方便起见,主流程我截了张图:
image.png
里面有关键的一步:即客户端验证服务端返回的公钥证书,这就是https的验证:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
只要是Https的请求,就会走到这个方法,该方法的作用就是处理服务器返回的证书, 需要在该方法中告诉系统是否需要安装服务器返回的证书,安装即代表公钥证书验证通过。
我们来通过AFNetworking的源码来查看一下它是怎么处理的,源码里有详细注释:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//NSURLAuthenticationChallenge 需要处理的挑战,里面包含了一个类型为 NSURLProtectionSpace 的属性
//NSURLProtectionSpace里面包含了 host port authenticationMethod等属性,还有服务器用于https验证的一些信息比如公钥、random_s等,这些都是服务器传过来的,authenticationMethod表示授权方式,对于AFNetworking来说,是通过比较NSURLAuthenticationMethodServerTrust来决定是否需要证书验证
/* 处理方式
NSURLSessionAuthChallengeUseCredential:使用指定证书
NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
NSURLSessionAuthChallengeRejectProtectionSpace:拒绝挑战,并尝试下一个验证保护空间,忽略证书参数
*/
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
/* NSURLCredential为一个证书,它决定了我们如何处理这个挑战,它的初始化方法有:
//一般用于需要处理身份验证401的错误验证
- (instancetype)initWithUser:(NSString *)user password:(NSString *)password persistence:(NSURLCredentialPersistence)persistence
//通过钥匙串中取得一个客户端证书来创建证书,一般用于服务端进行双向认证
+ (NSURLCredential *)credentialWithIdentity:(SecIdentityRef)identity certificates:(nullable NSArray *)certArray persistence:(NSURLCredentialPersistence)persistence
//通过服务端传回的SecTrustRef(包含待验证的证书和支持的验证方法等)对象创建证书,一般客户端需要验证服务端身份时使用,即Https中的客户端对服务端证书验证这一步,收到服务端的challenge,例如https需要验证证书等 ats开启
- (instancetype)initWithTrust:(SecTrustRef)trust
*/
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
//如果需要自定义处理当前的挑战,那么就通过block回调回去
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
//挑战凭证不一定都是进行HTTPS证书的信任,也可能是需要客户端提供用户密码或者提供双向验证时的客户端证书。当这个挑战凭证被验证通过时,请求便可以继续顺利进行
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//开始验证证书 ,只需要验证服务端证书是否安全(即https的单向认证,这是AF默认处理的认证方,其他的认证方式,只能由我们自定义Block的实现
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
//验证成功后,通过该证书创建一个NSURLCredential ,该证书会被传给服务端
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
//确定挑战方式
if (credential) {
//证书挑战
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
//默认挑战
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
//验证失败,表明客户端没办法处理,那么就Cancel掉
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
//默认挑战
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
//将验证完成的证书发给服务器
completionHandler(disposition, credential);
}
}
下面跳入真正做验证的方法:
先来认识几个属性:
- SSLPinningMode :返回SSL Pinning的类型。默认的是AFSSLPinningModeNone。
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone, //代表无条件信任服务器的证书,这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
AFSSLPinningModePublicKey, //代表会对服务器返回的证书中的PublicKey进行验证,客户端要有服务端的证书拷贝,验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
AFSSLPinningModeCertificate, //代表会对服务器返回的证书同本地证书全部进行校验,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
};
- pinnedCertificates:保存着所有的可用做校验的证书的集合。只要在证书集合中任何一个校验通过,evaluateServerTrust:forDomain: 就会返回true,即通过校验。
- allowInvalidCertificates:使用允许无效或过期的证书,默认是NO不允许。
- validatesDomainName:是否验证证书中的域名domain,默认是YES。
上一步生成证书的方法为:
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
//serverTrust为SecTrustRef类型的数据,就是待验证的对象。
-
SecTrustRef:其实就是一个容器,里面装了服务器需要验证证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端证书去匹配验证。
每一个SecTrustRef对象包含多个SecCertificateRef 和 SecPolicyRef。其中 SecCertificateRef 可以使用 DER 进行表示。 - domain:服务器域名,用于域名验证
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
//如果有域名 设置了允许信任无效或过期证书 需要验证域名 没有提供证书或者不验证证书 返回NO
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
return NO;
}
//用来装验证策略
NSMutableArray *policies = [NSMutableArray array];
//生成验证策略。如果要验证域名,就以域名为参数创建一个策略,否则创建默认的basicX509策略
if (self.validatesDomainName) {
// 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示为服务器证书验证创建一个策略,第二个参数传入domain,匹配主机名和证书上的主机名
//1.__bridge:CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化
//2.__bridge_transfer:常用在讲CF对象转换成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存
//3.__bridge_retained:(与__bridge_transfer相反)常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
// 如果不需要验证domain,就使用默认的BasicX509验证策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 为serverTrust设置验证策略
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
//不做SSL pinning,只在系统的信任机构列表里验证服务端返回的证书
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
//如果验证无效AFServerTrustIsValid,而且allowInvalidCertificates不允许自签,返回NO
return NO;
}
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default: //这里上面已经判断过了,执行到这里的话直接返回NO
return NO;
//客户端把服务端证书拷贝,存放在self.pinnedCertificates中
case AFSSLPinningModeCertificate: {
//全部校验(nsbundle .cer)
NSMutableArray *pinnedCertificates = [NSMutableArray array];
//把证书data,用系统api转成 SecCertificateRef 类型的数据,SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证
//serverTrust是服务器来的验证,有需要被验证的证书
// 把本地证书设置为根证书,
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//再去验证证书
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
//注意,这个方法和我们之前的锚点证书没关系了,是去从我们需要被验证的服务端证书,去拿证书链。
// 服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点
// 所有服务器返回的证书信息
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
//如果我们的证书中,有一个和它证书链中的证书匹配的,就返回YES
// 是否本地包含相同的data
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
//只验证公钥
NSUInteger trustedPublicKeyCount = 0;
// 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
//遍历服务端公钥
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
//判断如果相同 trustedPublicKeyCount+1
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}
return NO;
}
网友评论