通读AFN③--HTTPS访问控制(AFSecurityPolicy),Reachability(AFNetworkReachabilityManager)

这一篇主要介绍使用AFN如何访问HTTPS网站以及这些作法的实现原理,还有介绍AFN的网络状态监测部分AFNetworkReachabilityManager,这个模块会和苹果官方推荐的Reachability框架作一个对比。git

本文全部的代码都运行在iOS9.2的模拟器上,而且在info.plist对ATS作了适配:设置容许非法的加载Allow Arbitrary Loads为YES。
不要认为在info.plist添加NSAppTransportSecurity > NSAllowsArbitraryLoads为YES
就觉得弄懂iOS9网络适配了,有关具体细节问题请看南峰子的这篇文章App Transport Security(ATS)github

介于iOS有关HTTPS访问的认证过程代码并非特别常用,本文会用大量的篇幅介绍HTTPS认证的过程,并会经过系统的NSURLSession完成一些认证相关的代码,毕竟AFN就是使用了这些代码来实现对HTTPS网站的访问支持的。web

HTTPS网站访问过程当中,浏览器帮你作了什么

不一样于普通的HTTP请求,当访问一个HTTPS的网站时,浏览器会帮咱们不少隐藏的工做,这实际上是SSL通道创建的三次握手过程:
1.发起请求。
首先当输入完https网址敲击回车以后,浏览器首先向服务器发送一个须要访问的请求,这个请求中包含着浏览器SSL 协议的版本号,加密算法的种类,产生的随机数,以及其余服务器和客户端之间通信所须要的各类信息。
2.服务端返回证书。
服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其余相关信息,同时服务器还将向客户端传送本身的证书,这些信息被保存在客户端被称做'被保护空间'的地方。这里最关键的就是证书信息。
3.浏览器验证证书信息。
浏览器利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过时,发行服务器证书的CA 是否可靠,发行者证书的公钥可否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。
若是合法性验证没有经过,通信将断开;若是合法性验证经过,将继续进行第四步。
4.客户端向服务器发送“预主密码”。
浏览器随机产生一个用于后面通信的“对称密码”,而后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中得到)对其加密,而后将加密后的“预主密码”传给服务器。算法

4.1.若是服务器要求客户的身份认证(在握手过程当中为可选),用户不光要传给服务器“预主密码”,还需创建一个随机数而后对其进行数据签名,将这个含有签名的随机数和客户本身的证书也传给服务器。数组

4.2.若是不须要,则只将“预主密码”传给服务器,并直接进行第6步。
5.服务端身份验证(须要才进行)。
若是服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥可否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。
检验若是没有经过,通信马上中断;
若是验证经过,进行下一步。
6.浏览器、服务端各自生成通话密码。
服务器将用本身的私钥解开加密的“预主密码”,而后执行一系列步骤来产生主通信密码(客户端也将经过一样的方法产生相同的主通信密码)。
7.约定通话密码。
服务器和客户端用相同的主通信密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通信的加解密通信。同时在SSL 通信过程当中还要完成数据通信的完整性,防止数据通信中的任何变化。
8.浏览器通知服务器已准备就绪。
客户端向服务器端发出信息,指明后面的数据通信将使用的步骤⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
9.服务端通知浏览器已准备就绪。
服务器向客户端发出信息,指明后面的数据通信将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
10.开始数据通信。
SSL 的握手部分结束,SSL安全通道创建完成,开始进行数据通信开始,通信过程当中客户和服务器开始使用相同的对称密钥。
若是以https://www.baidu.com为例,这时候已经表现为baidu的主页打开了,可是SSL加密通道在下次请求的时候不用再次创建。xcode

对于访问的过程当中,一般会在第3步出现问题,以12306的购票页面为例:
当进行到第3步的时候,浏览器验证为:发行服务器证书的CA是不可靠的,能够在Chrome的地址栏中点击被打了红叉的锁来查看这个页面的证书颁发机构,
12306HTTPS证书
咱们能够搜索到这个命名为'SRCA'的机构其实是‘中铁认证中心’也就是12306本身的认证系统,它是用了本身的认证系统给本身颁发了一个SSL加密证书,而Chrome怎么会承认它呢。顺便看了一下百度的证书:
baiduHTTPS证书
这是一个由美国Symantec Trust Network组织颁发的证书,是一个比较权威的证书颁发机构,几乎在全部的浏览器中都是承认的。而baidu使用的证书是这个机构的根证书的子证书,而之因此浏览器能承认它,是由于根证书经过webtrust国际认证,并已经内置到各大浏览器如谷歌,火狐,微软等系统中。
那么这毕竟只是浏览器默认的一种认证方式,毕竟咱们仍是须要访问12306的,这里就要改变一下第3步验证的结果,在浏览器中,咱们能够手动选择信任,而后继续向下进行。
手动信任证书
这样就能访问这些网站了。浏览器

使用系统的NSURLSession模拟浏览器完成HTTPS的证书认证

与浏览器的验证过程类似,iOS的HTTPS验证过程也要走相似的步骤,不过不用担忧的是,不少过程咱们也不须要处理,只须要处理好第3步就好了,当咱们进行访问一个HTTPS网站时,当走到第二步的时候,也就是服务器返回证书时,须要咱们在本地本身完成证书信任的过程,若是使用session建立的task进行网络访问,这时候就会进入到- URLSession:didReceiveChallenge:completionHandler:这个代理方法中,这时候已经完成了HTTPS访问的第二步,session会让咱们在这个方法中完成第3步的过程。这个方法的参数有以下的解释:安全

参数 解释
challenge 一个包含了受权请求的对象
completionHandler 你的代理方法必定会调用的一个handler. 它的参数是
disposition—描述challenge如何被处理的几个常量中的一个
credential—若是disposition是NSURLSessionAuthChallengeUseCredential,credential是受权验证时会被使用到的凭据,其余状况为NULL.

challenge参数须要另外说明的是challenge是一个NSURLAuthenticationChallenge对象,表明着进行https请求进行时,服务端发送过来的质询,当接收到质询以后就要开始进行客户端的验证了。服务器

这个对象中最重要的属性就是protectionSpace它表明着对须要验证的受保护空间的验证,是一个NSURLProtectionSpace类型的对象。NSURLProtectionSpace对象包含请求的主机host、端口号port、代理类型proxyType、使用的协议protocol、服务端要求客户端对其验证的方法authenticationMethod等重要的信息,还有表明着服务器SSL传输状态的SecTrustRef类型的属性serverTrust,不过当且仅当authenticationMethod为NSURLAuthenticationMethodServerTrust这个属性值才不为Nil.网络

这里还要说明一下服务端指定的验证方法的类型,验证方法的类型有不少种,这里再也不一一列举,咱们一般会见到这样几种类型:

NSURLAuthenticationMethodHTTPBasic 
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodServerTrust

其中HTTP Basic、HTTP Digest与NTLM认证都是基于用户名/密码的认证,ClientCertificate(客户端证书)认证要求从客户端上传证书。客户端须要按照服务端指定的认证方法进行认证,不然可能会按照错误处理。例如使用HTTP Basic方式,客户端须要将用户名和密码信息放到凭据中,而后传递给服务端;若是使用的是ServerTrust方式,那么客户端就要将信任的凭据发给服务端。
通常在HTTPS访问的第3步过程当中,服务端要求的认证方法几乎老是ServerTrust方式。有遇到过一些网络代理工具使用HTTP Digest的验证方式,在浏览器端进行访问的时候就弹出一个要求输入帐号和密码的弹窗。

对于completionHandler参数是一个最终处理凭据的回调,要求在建立好包含验证信息的凭据以后必须调用,这样才会将验证的信息发送给服务端,也就意味着第3步的完成,开始进行第4步。
它的第一个参数是处理的选项,是一个枚举类型:

typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) {
    NSURLSessionAuthChallengeUseCredential = 0,   // 使用服务器发回的凭据,不过可能为空     
    NSURLSessionAuthChallengePerformDefaultHandling = 1,  // 默认的处理方法,凭据参数会被忽略
    NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,  //取消整个请求,忽略凭据参数 
    NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 此次质询被拒绝,下次再试 ,凭据参数被忽略
} NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0);

理清上面的思路以后,咱们能够试一试使用系统的session访问HTTPS网站了:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    [[self.session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"%@", error);
            return ;
        }
        NSLog(@"%@", response);
    }] resume];
}

#pragma mark - NSURLSessionDelegate

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
    // 判断服务器的身份验证的方法是不是:ServerTrust方式
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        
        // 建立一个新凭据,这个凭据指定了'握手'是被信任的
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        
        if (credential != nil) {
            // 完成'处置',将信任凭据发给服务端
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        }
        // 若是credential == nil 如下回调会自动完成
        // completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential);
    }
}

由于咱们使用的是使用第2步中服务端传回来的证书,因此即便是对付https://kyfw.12306.cn/otn/leftTicket/init这样的流氓页面也一样是能够的。可是对于iOS9来讲并非这样,必须设置了Allow Arbitrary Loads为YES才会达到预期效果。

对于AFN,不管实在iOS9以前仍是iOS9以后,当访问https://kyfw.12306.cn/otn/leftTicket/这个页面的时候都会走不通,这是由于AFN对于自签名的HTTPS网站有着特殊的验证(有关验证细节,请看本文下一部分),必须证书提早导入到项目中,将Chrome中的证书导入到项目中,请参见下图:
Chrome生成证书
将生成的证书文件kyfw.12306.cn.cer加入到xcode项目中,使用AFN按照以下方式调用便可:

NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil];

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
manager.securityPolicy.allowInvalidCertificates = YES;

[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSLog(@"%@",error);
}];

这样便能正确的访问自签名的网站了。

AFN实现HTTPS访问的细节

说了那么多如何使用代码访问HTTPS网站,那么AFN是如何实现的呢,AFURLSessionManager中实现了- URLSession:didReceiveChallenge:completionHandler:代理方法:

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.taskDidReceiveAuthenticationChallenge) {
        disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
    } else {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                disposition = NSURLSessionAuthChallengeUseCredential;
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            } else {
                disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

它的思路上这样的
若是主动经过manger的setTaskDidReceiveAuthenticationChallengeBlock:方法传递了taskDidReceiveAuthenticationChallenge的值那么,会按照传入的block处理此次质询,
若是没有传入就走AFN处理方式(else分支):

若是验证方法为ServerTrust就会使用securityPolicy属性的方法针对host评判serverTrust的合法性,若是成功了就会使用服务端传来的证书进行处理,失败了则会拒绝本次质询。

若是验证方法不是ServerTrust,则使用默认的处理方式(NSURLSessionAuthChallengePerformDefaultHandling)处理。

那么,能够看出,这里最关键的就是评判合法性的过程了,咱们重点来看一下。评判合法性的方法被定义在AFSecurity类中,是这个类惟一的对象方法:

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        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) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        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;
            }
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            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;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

这段长度为60行的代码实现了这样的过程:
第一个if分支是对自签名访问设立条件:
domain不存在,或者
不容许无效证书,或者
不须要验证域名,或者
SSLPinningMode不是AFSSLPinningModeNone,并且必须上传了证书文件。若是是走了这个分支,就要求若是想要实现自签名的HTTPS访问成功,必须设置pinnedCertificates,且不能使用defaultPolicy,由于不能SSLPinningMode属性是readonly的,而defaultPolicy在建立的时候已经设置SSLPinningMode属性为AFSSLPinningModeNone。(咱们刚才的实现方案就是在这条分支下完成的)

接下来是这样一块代码:

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) {
    return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
    return NO;
}

它完成的工做是:
先用policies数组组装验证策略,在经过SecTrustSetPolicies函数给serverTrust设置验证策略,不过AFN并无接收函数的返回值,查看是否设置成功,不知道是为何。
当SSLPinningMode为AFSSLPinningModeNone时,若是容许无效的证书(allowInvalidCertificates = YES)直接返回评测成功,若是不容许,按照刚才的验证策略验证,返回的是验证的结果。
当SSLPinningMode不是AFSSLPinningModeNone时,若是既没有验证成功又不容许无效证书,则直接返回评测失败。
(这里让我想到了另外一种访问12306实现的方案:

manager.securityPolicy.validatesDomainName = NO;
manager.securityPolicy.allowInvalidCertificates = YES;

既不用使用证书,也不用本身建立securityPolicy。
)

接下来看一下那个长长的switch:

若是self.SSLPinningMode是AFSSLPinningModeCertificate:取出self.pinnedCertificates中的全部证书,经过SecTrustSetAnchorCertificates函数设置证书验证策略,失败则直接返回评测失败,不然检查本地的证书是否包含服务端的证书
,若是是返回评测成功,不然返回评测失败。

若是self.SSLPinningMode是AFSSLPinningModePublicKey:取出服务端证书的全部公钥,和self.pinnedPublicKeys中全部公钥,遍历检查有没有相等的两项,有则返回评测成功。我尝试给securityPolicy的pinnedPublicKeys赋值一个公钥集合,可是它并无对外提供接口,self.pinnedPublicKeys是一个私有属性,而且是计算型的,是从本地的证书self.pinnedCertificates中提取出来的。

有关AFSecurityPolicy最核心的部分基本上将完了,最后咱们仍是要总结一下,访问可恶的12306的两种方法:

// 方式一 两句就能够
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy.validatesDomainName = NO; // 关键语句1
manager.securityPolicy.allowInvalidCertificates = YES; // 关键语句2
[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];


// 方式二 须要将证书导入到项目中
// 准备:将证书的二进制读取,放入set中
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil];

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set]; // 关键语句1
manager.securityPolicy.allowInvalidCertificates = YES; // 关键语句2
[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];

AFN的AFNetworkReachabilityManager和Reachability

有关AFNetworkReachabilityManager使用比较简单,不作太多的解释,只是罗列一些注意点。
AFN开启必须开启监控以后才能获取到新的网络状态,若是不开启各类网络状态都为不可到达,例如

AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager]; 

NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 始终是0
NSLog(@"%zd", reachabilityManager.isReachable);
NSLog(@"%zd", reachabilityManager.isReachableViaWWAN);

即便开启了网络监控,也没法再第一时间获取到网络状态,例以下面的代码执行以后,第一时间查看各类状态依然不可达,这是由于它会在网络情况改变时,异步改变单例中存储的状态。

AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
[reachabilityManager startMonitoring]; // 从开启监控  到获得下列值须要必定的时间
NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 马上调用为0 ,过一段时间后准确
NSLog(@"%zd", reachabilityManager.isReachable); // 马上调用为0 ,过一段时间后准确
NSLog(@"%zd", reachabilityManager.isReachableViaWWAN); // 马上调用为0 ,过一段时间后准确

其实我使用较多的仍是Reachability框架,
Reachability具备获取实时网络状态的-currentReachabilityStatus方法,不须要开启监控,只要用实例调用便可。
Reachability一样能够进行网络状态改变的监控,能够用-startNotifier方法开启,可是无法传入回调。可是每当网络状态改变的时候会发送一个kReachabilityChangedNotification通知,能够接收这个通知完成回调。

相关文章
相关标签/搜索