首页 > 系统 > iOS > 正文

详解iOS开发 - 用AFNetworking实现https单向验证,双向验证

2019-10-21 18:49:41
字体:
来源:转载
供稿:网友

自苹果宣布2017年1月1日开始强制使用https以来,htpps慢慢成为大家讨论的对象之一,不是说此前https没有出现,只是这一决策让得开发者始料未及,博主在15年的时候就做过https的接口,深知此坑之深,原因就是自身对这方面知识不了解加上网上的资料少,除此外还有博客不知对错就互相转载,导致当时网上几乎找不到能用的代码,这一点,博主说的毫不夸张。

鉴于此,博主一直想填一下这个坑,多增加一些正确的代码,来供广大开发者使用,后来一直被搁置,经过尝试后,博主现将整理好的代码发布在这里,希望能帮到焦急寻找的开发者。

1.先来说说老的AFNetworking2.x怎么来实现的

博主在网上看过几篇帖子,其中说的一些方法是正确的,但是却并不全对,由于那几篇博客几乎一样,博主不能确定最早的那篇是谁写的,所以就重新在下面说明下方法:

1)倒入client.p12证书;

2)在plist文件做如图配置:

ios开发,单向https,ios,https双向验证,os,https双向认证

3)在AFNetworking中修改一个类:

ios开发,单向https,ios,https双向验证,os,https双向认证

找到这个文件,在里面增加一个方法:

- (OSStatus)extractIdentity:(CFDataRef)inP12Data toIdentity:(SecIdentityRef*)identity {   OSStatus securityError = errSecSuccess;  CFStringRef password = CFSTR("证书密码");   const void *keys[] = { kSecImportExportPassphrase };  const void *values[] = { password };  CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);  CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);   securityError = SecPKCS12Import(inP12Data, options, &items);  if (securityError == 0)     {    CFDictionaryRef ident = CFArrayGetValueAtIndex(items,0);     const void *tempIdentity = NULL;     tempIdentity = CFDictionaryGetValue(ident, kSecImportItemIdentity);    *identity = (SecIdentityRef)tempIdentity;    }   if (options) {      CFRelease(options);    }  return securityError;}

再修改一个方法:

用下面的这段代码替换NSURLConnectionDelegate中的同名代码,

- (void)connection:(NSURLConnection *)connectionwillSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{  NSString *thePath = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];  //倒入证书    NSLog(@"thePath===========%@",thePath);  NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];  CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;  SecIdentityRef identity = NULL;  // extract the ideneity from the certificate  [self extractIdentity :inPKCS12Data toIdentity:&identity];  SecCertificateRef certificate = NULL;  SecIdentityCopyCertificate (identity, &certificate);  const void *certs[] = {certificate};  //            CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);  // create a credential from the certificate and ideneity, then reply to the challenge with the credential  //NSLog(@"identity=========%@",identity);  NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:nil persistence:NSURLCredentialPersistencePermanent];  //      credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];  [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];}

4)发起请求

 NSString *url = @"xxxxxxxxxx";  // 1.获得请求管理者  AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];  //2设置https 请求  AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];  securityPolicy.allowInvalidCertificates = YES;  mgr.securityPolicy = securityPolicy;  // 3.发送POST请求  [mgr POST:url parameters:nil success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {    NSLog(@"responseObject: %@", responseObject);  } failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {    NSLog(@"Error: %@", error);  }];

到此,老版的AFNetworking请求https接口的双向验证就做完了,但是有一个问题,这里需要改动AFNetworking的代码,何况新的AFNetworking已经有了,为了保持代码的活力,老的应该摒弃的,而且更新pods后肯定替换的代码就没了,也是一个问题,不要急,下面来说说怎么用新的AFNetworking,并解决被pods更新替换代码的问题。

最后再说一点,使用老的AF来请求,只用到了client.p12文件,并没有用到server.cer,在新的里面是有用到的,猜测可能是客户端选择信任任何证书导致的,就变成了单向的验证。

Demo放在最后

2.来说说新的AFNetworking3.x怎么来实现的

1)倒入client.p12和server.cer文件

2)plist内的设置,这是和上面一样的:

ios开发,单向https,ios,https双向验证,os,https双向认证

3)这里可不需要修改类里面的代码,但是这里需要重写一个方法:

 NSString *url = @"https://test.niuniuhaoguanjia.com/3.0.0/?service=City.GetCityList";  NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];  NSData *certData = [NSData dataWithContentsOfFile:certFilePath];  NSSet *certSet = [NSSet setWithObject:certData];  AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:certSet];  policy.allowInvalidCertificates = YES;  policy.validatesDomainName = NO;  _manager = [AFHTTPSessionManager manager];  _manager.securityPolicy = policy;  _manager.requestSerializer = [AFHTTPRequestSerializer serializer];  _manager.responseSerializer = [AFHTTPResponseSerializer serializer];  _manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/plain", nil];  //关闭缓存避免干扰测试r  _manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;  [_manager setSessionDidBecomeInvalidBlock:^(NSURLSession * _Nonnull session, NSError * _Nonnull error) {    NSLog(@"setSessionDidBecomeInvalidBlock");  }];  //客户端请求验证 重写 setSessionDidReceiveAuthenticationChallengeBlock 方法  __weak typeof(self)weakSelf = self;  [_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing*_credential) {    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 {      // client authentication      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 class]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;  }];

4)发起请求

//第三步和这一步代码是放在一起的,请注意哦  [_manager GET:url parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {  } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];    NSLog(@"JSON: %@", dic);  } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {    NSLog(@"Error: %@", error);    NSData *data = [error.userInfo objectForKey:@"com.alamofire.serialization.response.error.data"];    NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];    NSLog(@"%@",str);  }];

另外还要加上一个方法:

+(BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {  OSStatus securityError = errSecSuccess;  //client certificate password  NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"证书密码"                                 forKey:(__bridge id)kSecImportExportPassphrase];  CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);  securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge 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(@"Failedwith error code %d",(int)securityError);    return NO;  }  return YES;}

没错,我们是要封装一下,可是要怎么封装呢?博主尝试了集中都失败了,真是百思不得解,相信主动去封装的开发者也会碰到封装后请求失败的问题,也许你成功了,但是这里需要注意一个在block内使用变量的问题,具体的可以去看博主怎么封装的。

到这里,新的AF请求https就已经结束了,想看封装的,Demo放在最后。

3.单向验证

说到这个,不得不说一下网上的很多方法,都把单向验证当作双向的,其实也是并不理解其原理,关于原理,请看这里
代码实现AF都是一样的:

//AF加上这句和下面的方法  _manager.securityPolicy = [self customSecurityPolicy];/**** SSL Pinning ****/- (AFSecurityPolicy*)customSecurityPolicy {  NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];  NSData *certData = [NSData dataWithContentsOfFile:cerPath];  AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];  [securityPolicy setAllowInvalidCertificates:YES];  NSSet *set = [NSSet setWithObjects:certData, nil];  [securityPolicy setPinnedCertificates:@[certData]];  /**** SSL Pinning ****/  return securityPolicy;}

4.Demo下载福利

因为证书安全问题,Demo 里的证书博主删除了,请见谅,请大家放入自己的证书。

老的AF访问httpsDemo

新的AF访问httpsDemo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持VEVB武林网。


注:相关教程知识阅读请移步到IOS开发频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表