本文主要讲AFNetworking对https的处理方式,以及iOS下访问https的方法。
HTTP与HTTPS
HTTP是超文本传输协议,它是互联网数据通信的基础。
HTTPS是超文本传输安全协议,简单讲是HTTP的安全版。
HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包,HTTP默认端口是80,HTTPS是443。
HTTPS在HTTP请求的基础上多加了一个证书认证的流程,认证通过之后,数据开始加密传输。这个认证过程叫做SSL握手。
具体的握手过程可以参考:
《SSL握手过程》
《SSL/TLS握手过程详解》
SSL握手大致过程:
①客户端向服务器传送客户端 SSL 协议版本号、它所支持的加密算法列表、随机数等信息。
②服务器从加密列表中选择一种加密算法,并和它的证书、第二个随机数一起发给客户端。
③客户端验证证书是否有效,如果验证没有通过,通讯将断开;如果验证通过,将继续进行。
④客户端产生第二个随机数,并用服务器的证书中的公钥对其加密后传给服务器,服务器使用私钥解密得到第三个随机数。
⑤客户端与服务器根据三个随机数生成对称加密的密钥。
⑥SSL的握手部分结束,SSL安全通道的数据通讯开始。
⑦客户端与服务器之后的通讯信息加密后传输,由于数据是加密过的,所以就算请求被截取也是安全的。
进行 https 请求时,我们只需要负责第③步的证书验证,其它的都交给系统去做。系统会回调代理方法让我们去进行证书验证。
下面先分析一下AFNetworking的验证方法。
AFNetworking对HTTPS的处理
AFNetworking在AFURLSessionManager类中实现了系统的代理方法
-URLSession:task:didReceiveChallenge:completionHandler:
和
-URLSession:didReceiveChallenge:completionHandler:
,两个方法中的处理代码是相同的。
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
// 默认使用默认方式处理证书,这种方式无法验证通过自签名证书
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// 如果主动通过-setTaskDidReceiveAuthenticationChallengeBlock:方法设置处理方法,则按照block设置的方法处理
// 如果没有就走AF的处理方式(else分支)
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
// 如果验证方法为NSURLAuthenticationMethodServerTrust,说明服务器端需要客户端返回一个根据认证挑战的保护空间提供的信任(即challenge.protectionSpace.serverTrust)产生的挑凭据,用+credentialForTrust方法创建
// 如果验证方法不是NSURLAuthenticationMethodServerTrust,则challenge.protectionSpace.serverTrust为nil
// 一般情况下,服务器的验证方式都是ServerTrust,如果不是,就用其它方法创建凭据(参考那个官方文档链接)
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 使用设置好的安全策略securityPolicy进行验证
// serverTrust:用于执行X.509证书信任评估。其实就是一个容器,装了服务器端需要验证的证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端的证书去匹配验证的。
// domain:服务器域名
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 如果验证通过,则通过challenge.protectionSpace.serverTrust创建一个新凭据,供NSURLSession判断
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
// 如果验证不通过,则取消此次网络请求
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 如果验证方法不是ServerTrust,则使用默认的方式处理
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
// 回调block
if (completionHandler) {
completionHandler(disposition, credential);
}
}
大体逻辑是:
- 如果设置了taskDidReceiveAuthenticationChallenge这个block,就直接回调这个block进行验证;
- 否则就进入else分支,else分支中,验证方法如果是NSURLAuthenticationMethodServerTrust,就使用AFSecurityPolicy类的
-evaluateServerTrust:forDomain:
方法进行验证; - 如果是别的验证方法,就使用默认方式处理;
- 在
-evaluateServerTrust:forDomain:
方法中如果验证通过就创建一个凭据对象,使用completionHanlder这个block传递给NSURLSession内部,让它进行之后的工作,没有验证通过就直接取消此次网络请求。
其中 X.509 是一种广泛使用的数字证书标准,它规范了公开密钥认证、证书吊销列表、授权证书、证书路径验证算法等。
该方法有两个重要的参数:challenge
和completionHandler
。
challenge
是进行https请求进行时,服务端发送过来的质询,它包含了验证请求的所有信息,当接收到质询之后客户端就要开始进行验证。
challenge
中最重要的属性是protectionSpace
,它是需要身份验证的空间保护的说明,包含了请求的主机、端口号、代理类型、使用协议、服务端要求客户端对其验证的方法和服务器的SSL传输状态等重要信息。服务端指定的验证方法的类型有以下几种:
NSURLAuthenticationMethodDefault
NSURLAuthenticationMethodHTTPBasic
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodHTMLForm
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodNegotiate
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodServerTrust
其中HTTP Basic、HTTP Digest与NTLM认证都是基于用户名/密码的认证,ClientCertificate认证要求从客户端上传证书。
一般在HTTPS访问的过程中,服务端要求的认证方法一般都是ServerTrust方式。
如果是HTTP Basic方式,客户端需要将用户名和密码信息放到凭据中,然后传递给服务端。
如果是ClientCertificate,表示双向认证,处理的方式是使用
-credentialWithIdentity:certificates:persistence:
来获取本地证书创建凭证,使用该凭证来进行连接。
更多认证方法的处理,可以参考苹果官方文档。
对于completionHandler
,在创建好包含验证信息的凭据之后必须调用,这样才会将验证信息发送给服务端。它的第一个参数的一个枚举,代表如何处理这个证书:
typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) {
NSURLSessionAuthChallengeUseCredential = 0, // 使用服务器发回的凭据,可能为空
NSURLSessionAuthChallengePerformDefaultHandling = 1, // 默认的处理方法,如果未执行此代理,则忽略凭据参数
NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, //取消整个请求,忽略凭据参数
NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 这次质询被拒绝,尝试下一个身份验证保护控件 ,忽略凭据参数
} ;
第二个参数表示凭据,创建好凭据后调用completionHandler,就能把验证信息发送给服务端,服务器证书的验证就完成了。
AFSecurityPolicy类中的判断方法-evaluateServerTrust:forDomain:
如下:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
// domain 域名(上步传来的challenge.protectionSpace.host)
// allowInvalidCertificates 允许无效证书(自签名证书),默认为NO
// validatesDomainName 验证域名,默认为YES
// SSLPinningMode 评估模式,默认为None
// pinnedCertificates 存放证书的集合
// 有域名,允许自签名证书,需要验证域名,由于要验证域名,所以SSLPinningMode不能为None,证书也不能为0个
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;
}
// policies用来存放验证策略,如果需要验证域名,则根据域名生成一个证书链验证策略对象,否则返回一个默认的X.509标准策略对象
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 给serverTrust设置策略验证,即告诉客户端如何验证serverTrust
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 当SSLPinningMode为AFSSLPinningModeNone时,如果是自签名证书则直接返回成功,如果不是,就评估serverTrust的证书
// 允许无效(自签名)证书就返回YES,否则就判断serverTrust是否有效
// AFServerTrustIsValid函数会调用系统评估证书函数,去系统根证书里去找是否有匹配的证书。
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 当SSLPinningMode不是AFSSLPinningModeNone时,如果既没有验证成功又不允许无效证书,则直接返回评测失败。
return NO;
}
switch (self.SSLPinningMode) {
// 前面已经处理了AFSSLPinningModeNone,所以这里直接返回NO
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
// 把证书data转成SecCertificateRef类型的数据(DER编码的X.509证书)
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 将pinnedCertificates设置成锚点证书(Anchor Certificate,设置之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
// serverTrust是服务器来的验证,包含需要被验证的证书。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
// 正常情况下自签在之前是验证通过不了的,如果把我们的自签名证书加入self.pinnedCertificates,在这里就能验证成功了。
// 再去调用之前的serverTrust去验证该证书是否有效
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// 这个方法和我们之前的锚点证书已经没关系了,而是从被验证的服务端证书拿证书链
// 去被验证的服务端证书取证书链
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
// 如果我们的证书中,有一个和证书链中的证书匹配就返回YES
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
// 公钥验证 AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
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;
}
}
}
// 有相同就返回YES
return trustedPublicKeyCount > 0;
}
}
return NO;
}
大体逻辑是:
- 给系统设置验证策略,AFSSLPinningModeNone模式下如果设置了允许无效证书,就返回YES,否则根据证书验证情况来返回。
- 其它模式下,如果没有允许无效证书,先以系统中的CA根证书为锚点证书来验证, 如果没有设置允许无效证书,并且证书也无效,就返回NO。
- AFSSLPinningModeCertificate模式下,先把pinnedCertificates中的证书设置为锚点证书,然后使用导入的锚点证书来验证(即用本地证书验证服务器证书),如果验证通过就获取服务器证书的证书链,看本地证书是否包含证书链中的证书,包含就返回YES。
- AFSSLPinningModePublicKey模式下,只验证服务器返回证书的公钥,不验证其它信息。
其中锚点证书是系统信任的证书。
iOS下访问https的方法
访问https分两种情况:
- 1、如果请求的https的证书是国际公认可靠CA颁发的,那么NSURLSession内部会通过验证,不会走代理方法,这时不论使用系统的NSURLSession还是AFNetworking,都不需要额外代码,与http请求代码完全相同。
- 2、如果使用自签名的证书,则需要实现代理方法,如果是iOS9以上,还要设置Allow Arbitrary Loads为YES。
1.使用苹果的NSURLSession进行自签名证书验证
// 需要导入该头文件
#import <AssertMacros.h>
// 声明属性
@property (nonatomic, assign) BOOL notValidatesSSL; // 不验证SSL证书
@property (nonatomic, assign) BOOL validatesDomainName; // 验证域名
@property (nonatomic, assign) BOOL allowInvalidCertificates; // 允许自签名证书
@property (nonatomic, strong) NSSet <NSData *> *pinnedCertificates;
- (void)configWithValidateSSL:(BOOL)validate {
if (validate) {
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"12306.cer" ofType:nil];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *cers = [[NSSet alloc] initWithObjects:cerData, nil];
self.pinnedCertificates = cers;
self.notValidatesSSL = NO;
self.validatesDomainName = YES;
self.allowInvalidCertificates = YES;
} else {
self.notValidatesSSL = YES;
self.validatesDomainName = NO;
self.allowInvalidCertificates = YES;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// 验证证书
[self configWithValidateSSL:YES];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://kyfw.12306.cn/otn/login/init"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error:%@",error);
} else {
NSLog(@"response:%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}
}];
[task resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host certificates:self.pinnedCertificates]) {
// 如果验证通过,则通过challenge.protectionSpace.serverTrust创建一个新凭据,供NSURLSession判断
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
// 如果验证不通过,则取消此次网络请求
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 如果验证方法不是ServerTrust,则使用默认的方式处理
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain certificates:(NSSet <NSData *> *)certificates {
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.notValidatesSSL == YES || self.pinnedCertificates.count == 0)) {
return NO;
}
// 设置验证策略
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (!ServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
// 开始验证
if (self.notValidatesSSL == YES) {
return self.allowInvalidCertificates || ServerTrustIsValid(serverTrust);
} else if (!ServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in certificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!ServerTrustIsValid(serverTrust)) {
return NO;
}
NSArray *serverCertificates = CertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
// 查看是否与证书链中的证书匹配
if ([certificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
static BOOL ServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}
static NSArray * CertificateTrustChainForServerTrust(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];
}
2.AFNetworking进行自签名证书验证
方法一、将自签名证书导入项目中进行校验
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];
// 设置SSLPinningMode,并传入证书
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
// 设置允许自签名证书
manager.securityPolicy.allowInvalidCertificates = YES;
[manager GET:@"https://kyfw.12306.cn/otn/login/init" parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
证书导入项目后,将证书传入AFSecurityPolicy,allowInvalidCertificates
设为YES,SSLPinningMode设为AFSSLPinningModeCertificate,AF内部就会进行验证。
方法二、不进行验证,可访问所有https网站
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
// 设置不验证域名且允许无效证书
// 这时-evaluateServerTrust:forDomain:方法不会进行验证而直接返回YES
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
manager.securityPolicy.validatesDomainName = NO;
manager.securityPolicy.allowInvalidCertificates = YES;
[manager GET:@"https://kyfw.12306.cn/otn/login/init" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
设置validatesDomainName
为NO,allowInvalidCertificates
为YES。设置PinningMode
为AFSSLPinningModeNone
表示不验证SSL证书,validatesDomainName表示是否需要验证域名,默认为YES, 如果要忽略本地证书(或本地无证书),就把它设为NO。 allowInvalidCertificates 表示是否允许无效证书(也就是自建的证书),默认为NO,当它设为YES时,AF内部不管证书是否可靠,都直接返回验证通过。
参考文章:
《iOS中对HTTPS证书链的验证》
《iOS安全系列之一:HTTPS》
《iOS安全系列之二:HTTPS进阶》
《AFN-HTTPS访问控制》
《写给iOS开发者的HTTPS指南》