事件缘起
任性的苹果要求6月1日起所有上架app都需要支持ipv6。尼玛赶紧检查下代码是否能够满足要求,检查是否不兼容IPv6点击这里。结果是一个大大的懵逼啊。项目使用的AFNetworking根本就没升级还停留在2.0阶段,顿时心中策马奔腾啊。果断升级之,本以为很easy,分分钟搞定的事情,结果。。。
问题
AFNetworking 2.0 ->AFNetworking 3.0发生了很大变化,废弃了很多的类的同时,基本构架也发生了很大的变化。
AFNetworking 2.0使用NSURLConnection的基础API。
AFNetworking 3.0现已完全基于NSURLSession的API,这降低了维护的负担,同时支持苹果增强关于NSURLSession提供的任何额外功能。(以后苹果所有基于网络的功能,都会使用NSURLSession来扩展。在NSURLConnection类中也说明 DEPRECATED: The NSURLConnection class should no longer be used. NSURLSession is the replacement for NSURLConnection
)为什么要使用NSURLConnection这里一篇文章从性能方面进行了分析
AFNetworking 3.0相对于2.0具体来说
- 弃用类
AFURLConnectionOperation
AFHTTPRequestOperation
AFHTTPRequestOperationManager
- 修改的类
下面的类包含基于NSURLConnection的API的内部实现。他们已经被使用NSURLSession重构:
UIImageView+AFNetworking
UIWebView+AFNetworking
UIButton+AFNetworking
3.0和2.0类对比
检查我们原来的代码实现,发现我们恰恰就使用了废弃的类AFHTTPRequestOperation。我们原来的网络库竟然一直不升级我也是醉了。对此我只能说:让我来。
使用最新的AFURLSessionManager来发送网络请求就是了。
NSURL *URL = [NSURL URLWithString:MyURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
[request setHTTPMethod:@"POST"];
[request setTimeoutInterval:10];
[request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPMaximumConnectionsPerHost = 1;
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (error) {
NSLog(@"没有网络--- error---%@", error);
} else {
NSLog(@"网络是通的");
NSDictionary* dict = responseObject;
}
}];
[dataTask resume];
发现打印结果:没有网络 NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
检查Info.plist中退回到http配置
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
也增加了。why?
结果:原因是苹果的 官方资料 说首先必须要基于TLS 1.2版本协议。然后证书的加密的算法还需要达到SHA256或者更高位的RSA密钥或ECC密钥,如果不符合,请求将被中断并返回nil。原来我们需要验证证书啊。
解决办法
在AFNetworking中,只要通过下面的代码,你就可以使用自签证书来访问HTTPS
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
//是否允许不信任的证书(证书无效、证书时间过期)通过验证 ,默认为NO
securityPolicy.allowInvalidCertificates = YES;
这么做有个问题,就是你无法验证证书是否是你的服务器后端的证书,给中间人攻击留下了漏洞,可以通过重定向路由来分析伪造你的服务器端打开了大门。
AFNetworking是允许内嵌证书的,通过内嵌证书,AFNetworking就通过比对服务器端证书、内嵌的证书、站点域名是否一致来验证连接的服务器是否正确。由于CA证书验证是通过站点域名进行验证的,如果你的服务器后端有绑定的域名,这是最方便的。将你的服务器端证书,如果是pem格式的,用下面的命令转成cer格式
openssl x509 - in <你的服务器证书>.pem -outform der -out test.cer
然后将生成的test.cer文件,如果有自建ca,再加上ca的cer格式证书,引入到app的bundle里,然后在网络请求的地方做如下设置
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];
或者
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
如果app的bundle没有这个证书,会报错:
In order to validate a domain name for self signed certificates, you MUST use pinning.
这个pinning,指的是证书锁定,意思就是只有client包含的证书和服务器的证书一致时,才能通过验证。
查看AFNetworking的源代码,有如下核心方法
- evaluateServerTrust:forDomain:详情参考这里
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
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) {
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;
}
// 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);
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;
}
关于签名证书:AFnetworking默认会去程序中寻找所有cer文件,并找符合要求的。当然我们也可以指定的cer文件名称:
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPMaximumConnectionsPerHost = 1;
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModeCertificate];
NSString *certificatePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"cer"];
NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];
NSSet *certificateSet = [[NSSet alloc] initWithObjects:certificateData, nil];
[securityPolicy setPinnedCertificates:certificateSet];
securityPolicy.allowInvalidCertificates = YES;
securityPolicy.validatesDomainName = NO;
manager.securityPolicy = securityPolicy;
至此AFSecurityPolicy就只会比对服务器证书和内嵌证书是否一致,不会再验证证书是否和站点域名一致了。
这么做为什么是安全的?了解HTTPS的人都知道,整个验证体系中,最核心的实际上是服务器的私钥。私钥永远,永远也不会离开服务器,或者以任何形式向外传输。私钥和公钥是配对的,如果事先在客户端预留了公钥,只要服务器端的公钥和预留的公钥一致,实际上就已经可以排除中间人攻击了。
最后
最后完整验证证书代码
NSURL *URL = [NSURL URLWithString:URL_FRONT];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
[request setHTTPMethod:@"POST"];
[request setTimeoutInterval:10];//设置超时
[request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;// 设置缓存策略
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPMaximumConnectionsPerHost = 1;
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
// 安全验证
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModeCertificate];
NSString *certificatePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"cer"];
NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];
NSSet *certificateSet = [[NSSet alloc] initWithObjects:certificateData, nil];
[securityPolicy setPinnedCertificates:certificateSet];
securityPolicy.allowInvalidCertificates = YES;
securityPolicy.validatesDomainName = NO;
manager.securityPolicy = securityPolicy;
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (error) {
NSLog(@"没有网络--- error---%@", error);
} else {
NSLog(@"网络是通的");
NSLog(@"%@ %@", response, responseObject);
}
}];
[dataTask resume];
如有错误,欢迎指正!