寫在前面:
1.相關https具體內容本篇就不再描述。
2.前幾天公司項目要求配置https雙向認證,由于是銀行業務,證書是cfca(中國金融認證中心)頒發的,因此 在這里我就不描述自簽名證書的具體。iOS自簽名證書,我沒有調試,據說是不認自簽名的。自簽名證書中,這里有篇博客講的非常好,這篇幫我在本地搭建了雙向認證的tomcat服務器。http://blog.csdn.net/jerryvon/article/details/8548802
3.這篇博客側重于 iOS客戶端相關內容的描述,OC語言,基于AFNetwork3.0版本。
本篇內容
1.https雙向認證中,iOS客戶端需要哪兩個證書,什么格式的。
2.ATS設置
3.普通https請求證書驗證代碼 ?與 ?uiwebview請求https頁面時驗證代碼
4.html中ajax請求https
1.服務器公鑰和p12文件。服務器公鑰網上很多代碼是.cer格式的,但是在AF3.0版本中,支持的是.der格式,這個坑請注意了。
對公鑰證書data的描述
這個方法添加的是公鑰證書的data數據
p12證書 對應的是服務器上的 .pfx證書(應該是,不太確定),pfx格式的可以轉換成p12證書,它里面包括了公鑰和私鑰,客戶端用p12證書應該是在做客戶端的驗證。
這是我自己配置的tomcat配置,p12證書對應的是 keystoreFile所對應的證書。
注意:在添加讀取證書的時候,Xcode7有個bug 但不是必現。當證書拖到項目中時,路徑讀不出來,找不到這個文件,但是Bundle Resource中卻偏偏存在,當時我一直認為是證書的問題,糾結了很久。
這里添加證書,一般可以解決無法讀取路徑的問題
這個證書路徑容易讀出來是 nil
2.ATS設置
這個也是我一個不是特別能想的通的問題,按道理說,這個是設置在iOS9之后請求http才會設置的,但是在請求https的時候我發現 不設置的話,證書并沒有進行驗證。
因此還是要設置一下。
這里的域名下,TLS傳輸協議需要服務器端的注意一下,iOS支持的是1.2版本開始。但是很多都是從1.0開始的。因此我們最好也配上去。
3.請求https服務源代碼
與正常的AFN請求http請求來說,添加一這一個設置
```
-(AFSecurityPolicy*) getCustomHttpsPolicy:(AFHTTPSessionManager*)manager{
//https 公鑰證書配置
NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"custom" ofType:@"der"];
NSData *certData = [NSData dataWithContentsOfFile:certFilePath];
NSSet *certSet = [NSSet setWithObject:certData];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:certSet];
policy.allowInvalidCertificates = YES;
policy.validatesDomainName = NO;//是否校驗證書上域名與請求域名一致
//https回調 客戶端驗證
[manager setSessionDidBecomeInvalidBlock:^(NSURLSession * _Nonnull session, NSError * _Nonnull error) {
NSLog(@"setSessionDidBecomeInvalidBlock");
}];
__weak typeof(manager)weakManger = manager;
__weak typeof(self)weakSelf = self;
//客戶端請求驗證 重寫 setSessionDidReceiveAuthenticationChallengeBlock 方法
[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing*_credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential =nil;
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if([weakManger.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;
}];
return policy;
}
```
```
+ (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"111111"
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;
}
```
UIWebview加載雙向認證https頁面
這里需要注意一下,這個需要設置
設置,請求忽略本地緩存在性能上會有所犧牲。在https請求中,據說90%多cpu消耗都是花費在證書的校驗過程當中。但是,當html頁面中使用的ajax請求方式也是https的時候,這個就必須重新驗證。原因是,當uiwebview驗證完證書,之后,再次啟動app,webview會根據connection返回的驗證歷史信息,直接認為客戶端是安全的,不再進行證書校驗,這個時候,html頁面里面的ajax請求就不能正常的發送,會導致無法正常運行。
主要代碼:
```
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *tmp = [request.URL absoluteString];
NSLog(@"request url :%@",tmp);
if ([request.URL.scheme rangeOfString:@"https"].location != NSNotFound) {
//開啟同步的請求去雙向認證
if (!_Authenticated) {
originRequest = request;
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
[webView stopLoading];
return false;
}
}
return YES;
}
```
```
#pragma NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSURLCredential * credential;
assert(challenge != nil);
credential = nil;
NSLog(@"----received challenge----");
NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];
if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSLog(@"----server verify client----");
NSString *host = challenge.protectionSpace.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:@"custom" ofType:@"der"];
NSData *certData = [NSData dataWithContentsOfFile:path];
NSMutableArray *pinnedCerts = [NSMutableArray arrayWithObjects:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData), nil];
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
NSLog(@"----client verify server----");
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 file not exist!");
}else{
NSData *pkcs12Data = [NSData dataWithContentsOfFile:p12];
if ([[self 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];
}
}
}
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
```
```
+ (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityErr = errSecSuccess;
//client certificate password
NSDictionary *optionsDic = [NSDictionary dictionaryWithObject:@"111111" forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityErr = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDic, &items);
if (securityErr == errSecSuccess) {
CFDictionaryRef mineIdentAndTrust = CFArrayGetValueAtIndex(items, 0);
const void *tmpIdentity = NULL;
tmpIdentity = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tmpIdentity;
const void *tmpTrust = NULL;
tmpTrust = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemTrust);
*outTrust = (SecTrustRef)tmpTrust;
}else{
return false;
}
return true;
}
```
```
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
_Authenticated = YES;
//webview 重新加載請求。
[localWebView loadRequest:originRequest];
[connection cancel];
}
```
還值得注意的是:在https方式加載html的時候,我查到大量的資料中,都是在
```
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
```
這三個方法去處理的,方式是:將收到的data拼成一個全局的變量,在連接完成時將data轉換成字符串,用加載本地html的方式進行加載。
這個方法不可行,在加載非本地html的時候。
js、css、圖片的引入都是根據相對路徑的
在html前端頁面中,會有大量的外部資源引用。當將html頁面元素當做字符串去加載的時候,就會存在html依賴的外部資源無法加載,html頁面會出現圖片找不到或者布局混亂的現象。當使用loadHTMLString中的baseURL作為相對路徑的指導時,在加載本地資源時,使用的是bundle,但是加載遠程資源時,就會觸發https證書驗證的方法,無限次的循環,整個app高消耗內存和cpu,關鍵是還加載不到。
因此,在收到pResponse的時候,直接斷掉connection,重新加載該頁面就ok。這個時候,客戶端和服務器都是相互信任的,ssl通道已經建立起來,大家可以愉快的進行操作了。
ajax https請求
這個其實是個偽問題,在https方法加載的頁面當中,已經建立起來ssl通道的web環境,直接就可以進行https請求。因此是不需要進行像客戶端那樣配置。
但是,如果是http方式加載的頁面中,進行https請求,那么久存在跨域的問題。這個在下篇中再進行解釋。
因此,在html頁面是以https方法加載的情況下,不用考慮證書的問題,直接把請求方式改成https,完事。
來源: http://www.lxweimin.com/p/13f6d134a59a