您的位置:首页 > 理论基础 > 计算机网络

iOS之支持https与ssl双向验证(包含:解决UIWebView加载不了https网页的图片,css,js等外部资源)

2016-11-30 17:07 1071 查看

一,说明

在WWDC 2016开发者大会上,苹果宣布了一个最后期限:到2017年1月1日 App Store中的所有应用都必须启用 App Transport Security安全功能。App Transport Security(ATS)是苹果在iOS 9中引入的一项隐私保护功能,屏蔽明文HTTP资源加载,连接必须经过更安全的HTTPS。苹果目前允许开发者暂时关闭ATS,可以继续使用HTTP连接,但到年底所有官方商店的应用都必须强制性使用ATS。

补充:强制ATS的日期为2017年1月1日现已延期. 具体日期还没出来,今年可以过个好年了.链接:https://developer.apple.com/news/?id=12212016b



项目说明: 一开始听到这个消息,我对ATS的概念一知半解. 经过5天的时间,终于搞好了,在此记录一下.  我的项目中用到AFNetwork3.x,ASIHttpRequest,UIWebView.   在文章后面会一一讲解. 

我们后台开发的同学,用了nginx服务器,直接将http转为HTTPS.开发只用了一周.

首先后台会给你提供很多的证书,但是客户端只要几个证书,下面根据你的需求,选择证书.



(1),如果你的项目只支持ATS,单向验证,不需要ssl双向验证,你只需要金色的ca.cer证书(注:可将ca.crt转为ca.cer);
用openssl 将crt转为cer. 下面是2条命令:



(2),如果你的项目支持ATS,和ssl双向认证,那么你需要ca.cer证书,server.cer证书,client.p12证书(注:client.p12是client导入到钥匙串,在导出来的.注意导出时要记住密码"xxx"),这3个证书.
(3,)如果你的项目除了用到AFNetworking,还用到ASIHttpRequest, 那么你还需要client.pfx证书.(注:client.pfx是后台给的);

在项目开始前,在下图将plist里面的字段整个删掉:



1,如果你的项目只要支持ATS,不需要ssl双向验证. 那么你叫后台代码注释ssl双向验证功能(如果后台开启双向验证),只支持单向验证就可以.
我的所有接口用的是AFN3.x 所以我不会说AFN2.x(因为2.x不支持ipv6).  
下面是代码说明:
//只支持ATS,单向验证
_manager=[SHHttpRequestClient sharedSessionManager];
_manager.responseSerializer=[AFHTTPResponseSerializer serializer];
_manager.requestSerializer.timeoutInterval=12;
SHAFSecurityPolicy *securitypolicy=[SHAFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
_manager.securityPolicy=securitypolicy;
_manager.securityPolicy.allowInvalidCertificates=YES;//是否允许CA不信任的证书通过
[_manager.securityPolicy setValidatesDomainName:NO];//是否验证主机名
__weak typeof(self) weakSelf = self;
[_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) {

SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
/**
*  导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA),请替换掉你的证书名称
*/
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
weakSelf.manager.securityPolicy.pinnedCertificates = [NSSet setWithObjects:caCert,nil];
SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
NSCAssert(caRef != nil, @"caRef is nil");
NSArray *caArray = @[(__bridge id)(caRef)];
NSCAssert(caArray != nil, @"caArray is nil");
OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
SecTrustSetAnchorCertificatesOnly(serverTrust,NO);
//NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([weakSelf.manager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
*_credential=credential;
return disposition;
}];
[_manager GET:path parameters:params progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
将上面的代码拷贝到你的项目中,如果发现你的https接口请求时崩溃,那么是校验服务端证书个数和客户端信任个数不一致,那么此时要修改源码.

由于服务端使用.jks是一个证书库,客户端获取到的证书可能不止一本,具体获取到基本可通过SecTrustGetCertificateCount方法获取证书个数,AFNetworking在evaluateServerTrust:forDomain:方法中,AFSSLPinningMode的类型为AFSSLPinningModeCertificate和AFSSLPinningModePublicKey的时候都有校验服务端的证书个数与客户端信任的证书数量是否一样,如果不一样的话无法请求成功,所以这边我就修改他的源码,当有一个校验成功时即算成功。
在AFSecurityPolicy.m的-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString
*)domain修改代码:
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)
NSUInteger trustedCertificateCount = 0;//这个地方修改
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);

for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
trustedCertificateCount++;//这个地方修改
//return YES;
}
}

return trustedCertificateCount == [serverCertificates count] - 1;//这个地方修改
//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 && ( trustedPublicKeyCount >= 1);//这个地方修改
//return trustedPublicKeyCount > 0;
}


同时,在AFHTTPSessionManager.m的

- (void)setSecurityPolicy:(SHAFSecurityPolicy
*)securityPolicy

方法中将

@throw [NSException exceptionWithName:@"Invalid Security Policy" reason:reason userInfo:nil];注释掉如下图:



完成,运行,发现所有的https的接口请求都有数据,但是加载的https的H5页面和https的网络图片加载不上,怎么解决? 在下面会讲解. 但是先将ssl双向验证讲完.

2,如果你的项目既要支持ATS,还需要ssl双向验证. 那么你叫后台开启ssl双向验证功能.
这个时候还需要server.cer证书和client.p12及client.p12对于的密码@"xxx".
下面是代码讲解:
- (void)startGetRequest:(NSDictionary *)params withPath:(NSString *)path{
_manager=[SHHttpRequestClient sharedSessionManager];
_manager.responseSerializer=[AFHTTPResponseSerializer serializer];
_manager.requestSerializer.timeoutInterval=12;
NSString *cerpth=[[NSBundle mainBundle]pathForResource:@"server" ofType:@"cer"];
NSData *cerdat=[NSData dataWithContentsOfFile:cerpth];
SHAFSecurityPolicy *securitypolicy=[SHAFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:[NSSet setWithObjects:cerdat,nil]];
_manager.securityPolicy=securitypolicy;
_manager.securityPolicy.allowInvalidCertificates=YES;//是否允许CA不信任的证书通过
[_manager.securityPolicy setValidatesDomainName:NO];//是否验证主机名
__weak typeof(self) weakSelf = self;
[_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) {
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
/**
*  导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA),请替换掉你的证书名称
*/
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
weakSelf.manager.securityPolicy.pinnedCertificates = [NSSet setWithObjects:caCert,nil];
SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
NSCAssert(caRef != nil, @"caRef is nil");
NSArray *caArray = @[(__bridge id)(caRef)];
NSCAssert(caArray != nil, @"caArray is nil");
OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
SecTrustSetAnchorCertificatesOnly(serverTrust,NO);
//NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([weakSelf.manager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {//自签证书验证, 是对服务端返回的证书与客户端本地证书验证是否一致
//disposition = NSURLSessionAuthChallengePerformDefaultHandling;
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client"ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];

if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];

if ([weakSelf extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge  NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition = NSURLSessionAuthChallengeUseCredential;
}
}

}
*_credential=credential;
return disposition;
}];
[_manager GET:path parameters:params progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
}
- (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef*)outTrust fromPKCS12Data:(NSData *)inPKCS12Data
{
OSStatus securityError = errSecSuccess;

NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:@"xxx" forKey:(id)kSecImportExportPassphrase];

CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((CFDataRef)inPKCS12Data,(CFDictionaryRef)optionsDictionary,&items);

if (securityError == 0) {
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void*tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void*tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"--------证书错误------- %d",(int)securityError);
return NO;
}
return YES;
}
完成,如果你上传的server.cer证书是没错的,那么求https的接口,是有返回数据的. 
3,关于webView加载H5页面的问题, 由于项目支持iOS7,所以这里只讲UIWebView. 
(1)如果你的服务端是单向验证,那么Https的H5是可以加载的.  
(2)如果你的H5的页面全都是https.那么需要ssl双向认证. 由于NSURLConnection被废了.所以这里只讲NSURLsession的双向验证.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if ([[[NSURL URLWithString:_baseUrl] scheme] isEqualToString:@"https"]) {
if (!isOnceTrue) {
isOnceTrue=NO;//全局定义一个BOOL类型的属性,只作为每当打开webview时一次性ssl双向验证
//开启同步的请求去双向认证
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"Mutual Author";
//NSURLSession *session = [NSURLSession sharedSession];
NSURLSession *session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:queue];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if ([error localizedDescription].length==0) {
[self loadHtmlorData];//证书验证正确,则会求H5页面
}
}];
[task resume];
[webView stopLoading];
return NO;
}
}
return YES;

#pragma mark --SSL
#pragma mark -- NSURLSession Delegate --

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
NSString *method = challenge.protectionSpace.authenticationMethod;
NSLog(@"challenge auth method:%@",method);
if ([method isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSString *host = challenge.protectionSpace.host;
NSLog(@"host:%@",host);

SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
BOOL validDomain = false;
NSMutableArray *polices = [NSMutableArray array];
if (validDomain) {
[polices addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)host)];
}else{
[polices addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)polices);
//pin mode for certificate
NSString *path = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:path];
NSMutableArray *pinnedCerts = [NSMutableArray arrayWithObjects:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData), nil];
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);

NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
return;
}

//client authentication
NSString *thePath = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSData *pkcs12Data = [NSData dataWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(pkcs12Data);
SecIdentityRef identity;

OSStatus ret = [self extractP12Data:inPKCS12Data toIdentity:&identity];
if (ret != errSecSuccess) {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,nil);
return;
}
isOnceTrue=YES;
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void *certs[] = {certificate};
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(NSArray *)CFBridgingRelease(certArray) persistence:NSURLCredentialPersistencePermanent];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}

- (OSStatus)extractP12Data:(CFDataRef)inP12Data toIdentity:(SecIdentityRef *)identity {
OSStatus securityErr = errSecSuccess;

CFStringRef pwd = CFSTR("xxx");
const void *keys[] = {kSecImportExportPassphrase};
const void *values[] = {pwd};

CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityErr = SecPKCS12Import(inP12Data, options, &items);

if (securityErr == errSecSuccess) {
CFDictionaryRef ident = CFArrayGetValueAtIndex(items, 0);
const void *tmpIdent = NULL;
tmpIdent = CFDictionaryGetValue(ident, kSecImportItemIdentity);
*identity = (SecIdentityRef)tmpIdent;
}

if (options) {
CFRelease(options);
}

return securityErr;
}
完成,UIWebview的ssl双向验证成功.  但是加载https的H5页面会出现布局混乱,页面显示不全.
这是因为H5相关联的请求都是http请求, 我暂时没发现客户端可以解决这个问题.在本博客更新部分已解决这个问题.请查看.
上面的UIwebview加载https的H5页面,同时要双向验证的问题没解决,这是我想,将所有的H5页面的https都换为http,这样就不存在页面混乱,显示不全的问题.   要写UIWebView成功加载http的H5页面同时又不影响ATS.
有2种方法:
第一:如下图所示;在plist文件中添加例外,(例外最好都有以下3个字段):



NSIncludesSubdomains 设为YES表示:该域名的ATS配置适配其子域名
NSExceptionRequiresForwardSecrecy 设为NO表示访问该域名时TLS可以不支持完全正向保密
NSExceptionAllowsInsecureHTTPLoads 设为YES表示该域名可以通过http请求,但是TLS版本要求还是1.2.

如果加载的http的H5页面的域名和你的服务器域名一样,那么你就不能通过添加例外解决该问题.只能通过第二种方法
第二:在iOS10以上可以通过设置Allow Arbitrary Loads in Web Content 来允许请求http,iOS9那么可以设置Allow Arbitrary Loads.如下图:



【注意】
NSAllowsArbitraryLoads
NSAllowsArbitraryLoadsInMedia
NSAllowsArbitraryLoadsInWebContent
NSExceptionAllowsInsecureHTTPLoads
任一属性置为
YES
或设置
NSExceptionMinimumTLSVersion
,都会触发App
Store的额外审核并需要提交对应说明.  但是对于以上设置,我不能保证审核一定能够通过,我选的是第二种方法.

 
此时通过AFN3.x请求接口可以成功,UIWebview加载http的H5页面也是正常显示的.那么对于记载https的网络图片如果不能正常显示,那么SDWebImage就要换一种方法,如下图:



如果https的网络图片还是不能显示,要么添加例外,要么将https改为http.提交时进行说明.
运行成功了,报了一个错:友盟的第三方不支持https



解决方法:添加例外:



4关于ASIHttpRequest的ssl双向验证的问题,那么需要client.pfx证书
以下是代码讲解:
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSData *PKCS12Data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"client" ofType:@"pfx"]];
[self extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data];
request1 = [ASIHTTPRequest requestWithURL:url];
[request1 setClientCertificateIdentity:identity];
[request1 setValidatesSecureCertificate:YES];
[request1 setDelegate:self];
[request1 startAsynchronous];
- (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef*)outTrust fromPKCS12Data:(NSData *)inPKCS12Data
{
OSStatus securityError = errSecSuccess;

NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:@"xxx" forKey:(id)kSecImportExportPassphrase];

CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((CFDataRef)inPKCS12Data,(CFDictionaryRef)optionsDictionary,&items);

if (securityError == 0) {
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failed with error code %d",(int)securityError);
return NO;
}
return YES;
}
OK,此时运行,报错:



原因是我们客户端证书链的顶层是自签的证书,不受信任.
解决方法:还是修改源码:在ASIHTTPRequest.m的-(void)startRequest方法中将
NSMutableDictionary *sslProperties = [NSMutableDictionary dictionaryWithCapacity:1];//替换为:
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates,
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot,
[NSNumber numberWithBool:NO],  kCFStreamSSLValidatesCertificateChain,
kCFNull,kCFStreamSSLPeerName,
nil];



保存,再次运行,请求成功.








注意一点,不要将你自已的服务器域名,添加到例外中.否则有伪装第三方服务器的嫌疑,苹果有办法检测的,后果是不能承受. 目前我的做法是所有的接口请求都是https,所有的UIWebView加载的H5页面都是http,图片也是http.因为我在info.plist中添加Allow Aribitrary Loads为YES.所以我会在提交审核时会详细解释,力求能审核通过.  例外澄清一点,文章开头说的16年底,所有app事强制ATS.是一位参加wwdc2016的人发的推特. 苹果并不是说17之后就不能用http的接口了.
只是希望开发者不要无脑的关闭ATS.并且要详细的给个解释.链接:http://www.jianshu.com/p/6d5e0f8b722b 和苹果官方文档.

----------------------------------------------------------------------------更新----------------------------------------------------------------------------------------------------------------

二,更新(重要)

1,现在项目使用AFN3.x  所有接口支持HTTPS.对此更新. :
证书;只要一个client.p12证书及对应的密码如:lovely_girl
现在以一个AFN3.x请求为例,代码如下

- (void)startRequest:(NSDictionary*)params path:(NSString*)path{

urlString=[NSStringstringWithFormat:@"%@%@",Httpsource,path];
_manager=[AFHTTPSessionManagermanager];
_manager.responseSerializer=[AFHTTPResponseSerializerserializer];
_manager.requestSerializer.timeoutInterval=12;
_manager.securityPolicy.allowInvalidCertificates=YES;
[_manager.securityPolicysetValidatesDomainName:NO];
[_manager.requestSerializersetValue:@"gzip"forHTTPHeaderField:@"Accept-Encoding"];

__weak typeof(self) weakSelf =self;
[_managersetSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session,NSURLAuthenticationChallenge *challenge,NSURLCredential *__autoreleasing *_credential) {

NSURLSessionAuthChallengeDisposition disposition =NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential =nil;
if ([challenge.protectionSpace.authenticationMethodisEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([weakSelf.manager.securityPolicyevaluateServerTrust:challenge.protectionSpace.serverTrustforDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredentialcredentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition =NSURLSessionAuthChallengeUseCredential;
}else {
disposition =NSURLSessionAuthChallengePerformDefaultHandling;
}
}else {
disposition =NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}else {
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundlemainBundle] pathForResource:@"client"ofType:@"p12"];
NSFileManager *fileManager =[NSFileManagerdefaultManager];

if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSDatadataWithContentsOfFile:p12];
if ([HttpRequestextractIdentity:&identity andTrust:&trustfromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredentialcredentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition =NSURLSessionAuthChallengeUseCredential;
}
}
}
*_credential=credential;
return disposition;
}];

[_managerPOST:urlStringparameters:params progress:^(NSProgress *_Nonnull uploadProgress) {
}success:^(NSURLSessionDataTask *_Nonnull task, id _Nullable responseObject) {
NSDictionary *dic=[NSJSONSerializationJSONObjectWithData:responseObject options:NSJSONReadingAllowFragmentserror:nil];
if ([_delegaterespondsToSelector:@selector(request:didReceiveData:)]) {
[_delegaterequest:selfdidReceiveData:dic];
}
}failure:^(NSURLSessionDataTask *_Nullable task, NSError *_Nonnull error) {
if ([_delegaterespondsToSelector:@selector(requestFailed:)]) {
[_delegaterequestFailed:self];
}
}];
if ([_delegaterespondsToSelector:@selector(requestStarted:)]) {
[_delegaterequestStarted:self];
}
}

+(BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef*)outTrust fromPKCS12Data:(NSData *)inPKCS12Data
{
OSStatus securityError =errSecSuccess;
NSDictionary *optionsDictionary = [NSDictionarydictionaryWithObject:@"lovely_girl"forKey:(id)kSecImportExportPassphrase];

CFArrayRef items =CFArrayCreate(NULL,0, 0,NULL);
securityError = SecPKCS12Import((CFDataRef)inPKCS12Data,(CFDictionaryRef)optionsDictionary,&items);

if (securityError ==0) {
CFDictionaryRef myIdentityAndTrust =CFArrayGetValueAtIndex (items, 0);
constvoid *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
constvoid *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"--------证书错误------- %d",(int)securityError);
returnNO;
}
returnYES;
}


只要写这2个方法,就可以完整的以一个自签证书,进行https接口请求.

2,对于上述使用NSURLsession双向认证记载HTTPS网页,导致页面不全.是因为图片,css,js等外部资源没有加载上.
 我看到我安卓同时能够完整的加载一个自签的https的网页,是因为安卓里面有个回调方法,网页每加载一个资源,都在这个回调方法里验证证书.  所以上面的NSURLsession只验证了一次,是不行的.所以显示不全.下面是解决方法.
 (该解决方法是在3年前的一个帖子上发现的).


新建HTTPSURLProtocol继承NSURLProtocol.

2-1,在appdelegate.m中注册,添加以下代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[NSURLProtocol registerClass:[HTTPSURLProtocol class]];
}

2-2,HTTPSURLProtocol.h

#import <Foundation/Foundation.h>

/**
该类用于处理https类型的ajax请求
*/
@interface HTTPSURLProtocol : NSURLProtocol
{
NSMutableData* _data;
NSURLConnection *theConncetion;
}

@end

2-3,HTTPSURLProtocol.m

#import "HTTPSURLProtocol.h"

static NSString * const URLProtocolHandledKey = @"这里任意字符串";
@implementation HTTPSURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)theRequest
{
//FIXME: 后来的https类型的reqeust应该也要被处理
NSString *scheme = [[theRequest URL] scheme];
if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
{
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:theRequest]) {
return NO;
}
//此处做个判断,app.xxxx.com是我们项目的域名.任何不属于我们域名的不做处理,这样可以加载https://weibo.com等第三方https网页
if (![[[theRequest URL] host] isEqualToString:@"app.xxxx.com"]) {
return NO;
}
return YES;
}
return NO;
}

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request
{
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
mutableReqeust = [self redirectHostInRequset:mutableReqeust];
return mutableReqeust;
}

- (void)startLoading
{

//theConncetion = [[NSURLConnection alloc] initWithRequest:self.request delegate:self];

NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//标示改request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
theConncetion = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
if (theConncetion) {
_data = [NSMutableData data];
}
}

- (void)stopLoading
{
// NOTE:如有清理工作,可以在此处添加
[theConncetion cancel];
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)requestA toRequest:(NSURLRequest*)requestB
{
return [super requestIsCacheEquivalent:requestA toRequest:requestB];
}

#pragma mark - NSURLConnectionDelegate

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
//响应服务器证书认证请求和客户端证书认证请求
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] ||
[protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLCredential* credential;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
//服务器证书认证
credential= [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
{
//客户端证书认证
//TODO:设置客户端证书认证

NSString *thePath = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(PKCS12Data);
SecIdentityRef identity;
// 读取p12证书中的内容
OSStatus result = [self extractP12Data:inPKCS12Data toIdentity:&identity];

//        if(result != errSecSuccess){
//            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
//            return;
//        }

SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);

const void *certs[] = {certificate};
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);

credential = [NSURLCredential credentialWithIdentity:identity certificates:(NSArray*)CFBridgingRelease(certArray) persistence:NSURLCredentialPersistencePermanent];
}

if (credential != nil)
{
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
else
{
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_data appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[[self client] URLProtocol:self didLoadData:_data];
[[self client] URLProtocolDidFinishLoading:self];

}

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[[self client] URLProtocol:self didFailWithError:error];

}
- (OSStatus)extractP12Data:(CFDataRef)inP12Data toIdentity:(SecIdentityRef *)identity {
OSStatus securityErr = errSecSuccess;

CFStringRef pwd = CFSTR("此处为证书密码");
const void *keys[] = {kSecImportExportPassphrase};
const void *values[] = {pwd};

CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityErr = SecPKCS12Import(inP12Data, options, &items);

if (securityErr == errSecSuccess) {
CFDictionaryRef ident = CFArrayGetValueAtIndex(items, 0);
const void *tmpIdent = NULL;
tmpIdent = CFDictionaryGetValue(ident, kSecImportItemIdentity);
*identity = (SecIdentityRef)tmpIdent;
}

if (options) {
CFRelease(options);
}

return securityErr;
}
+(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request
{
if ([request.URL host].length == 0) {
return request;
}

NSString *originUrlString = [request.URL absoluteString];
NSLog(@"-----链接导向---------->:%@",originUrlString);

NSString *originHostString = [request.URL host];
NSRange hostRange = [originUrlString rangeOfString:originHostString];
if (hostRange.location == NSNotFound) {
return request;
}
//此处可以将网页中链接的域名替换为自己的域名,防止劫持.
//    NSString *ip = @"app.xxxx.com";
//     // 替换域名
//    NSString *urlString;
//    if ([originUrlString isEqualToString:@"apis.map.qq.com"]) {
//        urlString=originUrlString;
//    }else if ([originUrlString isEqualToString:@"weibo.com"]){
//        urlString=originUrlString;
//
//    }else{
//        urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
//    }

//    NSURL *url = [NSURL URLWithString:originUrlString];
//    request.URL = url;

return request;
}
@end

OK,完成,解决了uiwebview加载不了图片,css,js等外部资源,同时可以访问微博等第三方https页面.并且对每个https的请求做处理,提升加载速度在5秒以内(安卓的超过10秒).

三,结束语:

如果对文章有什么不懂的,欢迎评论,也欢迎顶一个.
参考:http://bewithme.iteye.com/blog/1999031 http://www.cocoachina.com/ios/20160928/17663.html http://www.cocoachina.com/bbs/read.php?tid=1689632 http://www.cocoachina.com/ios/20151021/13722.html http://www.jianshu.com/p/7c89b8c5482a
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ATS ios ssl
相关文章推荐