由於蘋果規定2017年1月1日以後,所有APP都要使用HTTPS進行網絡請求,否則無法上架,因此研究了一下在iOS中使用HTTPS請求的實現。相信大家對HTTPS都或多或少有些了解,這裡我就不再介紹了,主要功能就是將傳輸的報文進行加密,提高安全性。
證書分為兩種,一種是花錢向認證的機構購買的證書,服務端如果使用的是這類證書的話,那一般客戶端不需要做什麼,用HTTPS進行請求就行了,蘋果內置了那些受信任的根證書的。另一種是自己制作的證書,使用這類證書的話是不受信任的(當然也不用花錢買),因此需要我們在代碼中將該證書設置為信任證書。
我這邊使用的是xca來制作了根證書,制作流程請參考http://www.2cto.com/Article/201411/347512.html,由於xca無法導出.jsk的後綴,因此我們只要制作完根證書後以.p12的格式導出就行了,之後的證書制作由命令行來完成。自制一個批處理文件,添加如下命令:
set ip=%1% md %ip% keytool -importkeystore -srckeystore ca.p12 -srcstoretype PKCS12 -srcstorepass 123456 -destkeystore ca.jks -deststoretype JKS -deststorepass 123456 keytool -genkeypair -alias server-%ip% -keyalg RSA -keystore ca.jks -storepass 123456 -keypass 123456 -validity 3650 -dname "CN=%ip%, OU=ly, O=hik, L=hz, ST=zj, C=cn" keytool -certreq -alias server-%ip% -storepass 123456 -file %ip%\server-%ip%.certreq -keystore ca.jks keytool -gencert -alias ca -storepass 123456 -infile %ip%\server-%ip%.certreq -outfile %ip%\server-%ip%.cer -validity 3650 -keystore ca.jks keytool -importcert -trustcacerts -storepass 123456 -alias server-%ip% -file %ip%\server-%ip%.cer -keystore ca.jks keytool -delete -keystore ca.jks -alias ca -storepass 123456
將上面加粗的ca.p12改成你導出的.p12文件的名稱,123456改為你創建證書的密碼。
然後在文件夾空白處按住ctrl+shift點擊右鍵,選擇在此處打開命令窗口,在命令窗口中輸入“start.bat ip/域名”來執行批處理文件,其中start.bat是添加了上述命令的批處理文件,ip/域名即你服務器的ip或者域名。執行成功後會生成一個.jks文件和一個以你的ip或域名命名的文件夾,文件夾中有一個.cer的證書,這邊的.jks文件將在服務端使用.cer文件將在客戶端使用,到這裡證書的准備工作就完成了。
由於我不做服務端好多年,只會使用Tomcat,所以這邊只講下Tomcat的配置方法,使用其他服務器的同學請自行查找設置方法。
打開tomcat/conf目錄下的server.xml文件將HTTPS的配置打開,並進行如下配置:
keystoreFile是你.jks文件放置的目錄,keystorePass是你制作證書時設置的密碼,netZone填寫你的ip或域名。注意蘋果要求協議要TLSv1.2以上。
首先把前面生成的.cer文件添加到項目中,注意在添加的時候選擇要添加的targets。
1.使用NSURLSession進行請求
代碼如下:
NSString *urlString = @" https://xxxxxxx "; NSURL *url = [NSURL URLWithString:urlString]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f]; NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request]; [task resume];
需要實現NSURLSessionDataDelegate中的URLSession:didReceiveChallenge:completionHandler:方法來進行證書的校驗,代碼如下:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { NSLog(@"證書認證"); if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) { do { SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; NSCAssert(serverTrust != nil, @"serverTrust is nil"); if(nil == serverTrust) break; /* failed */ /** * 導入多張CA證書(Certification Authority,支持SSL證書以及自簽名的CA),請替換掉你的證書名稱 */ NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自簽名證書 NSData* caCert = [NSData dataWithContentsOfFile:cerPath]; NSCAssert(caCert != nil, @"caCert is nil"); if(nil == caCert) break; /* failed */ SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert); NSCAssert(caRef != nil, @"caRef is nil"); if(nil == caRef) break; /* failed */ //可以添加多張證書 NSArray *caArray = @[(__bridge id)(caRef)]; NSCAssert(caArray != nil, @"caArray is nil"); if(nil == caArray) break; /* failed */ //將讀取的證書設置為服務端幀數的根證書 OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray); NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed"); if(!(errSecSuccess == status)) break; /* failed */ SecTrustResultType result = -1; //通過本地導入的證書來驗證服務器的證書是否可信 status = SecTrustEvaluate(serverTrust, &result); if(!(errSecSuccess == status)) break; /* failed */ NSLog(@"stutas:%d",(int)status); NSLog(@"Result: %d", result); BOOL allowConnect = (result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed); if (allowConnect) { NSLog(@"success"); }else { NSLog(@"error"); } /* kSecTrustResultUnspecified and kSecTrustResultProceed are success */ if(! allowConnect) { break; /* failed */ } #if 0 /* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */ /* since the user will likely tap-through to see the dancing bunnies */ if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError) break; /* failed to trust cert (good in this case) */#endif // The only good exit point NSLog(@"信任該證書"); NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential,credential); return [[challenge sender] useCredential: credential forAuthenticationChallenge: challenge]; } while(0); } // Bad dog NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,credential); return [[challenge sender] cancelAuthenticationChallenge: challenge];}
此時即可成功請求到服務端。
注:調用SecTrustSetAnchorCertificates設置可信任證書列表後就只會在設置的列表中進行驗證,會屏蔽掉系統原本的信任列表,要使系統的繼續起作用只要調用SecTrustSetAnchorCertificates方法,第二個參數設置成NO即可。
2.使用AFNetworking進行請求
AFNetworking首先需要配置AFSecurityPolicy類,AFSecurityPolicy類封裝了證書校驗的過程。
/** AFSecurityPolicy分三種驗證模式: AFSSLPinningModeNone:只是驗證證書是否在信任列表中 AFSSLPinningModeCertificate:該模式會驗證證書是否在信任列表中,然後再對比服務端證書和客戶端證書是否一致 AFSSLPinningModePublicKey:只驗證服務端證書與客戶端證書的公鑰是否一致 */ AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate]; securityPolicy.allowInvalidCertificates = YES;//是否允許使用自簽名證書 securityPolicy.validatesDomainName = NO;//是否需要驗證域名,默認YES AFHTTPSessionManager *_manager = [AFHTTPSessionManager manager]; _manager.responseSerializer = [AFHTTPResponseSerializer serializer]; _manager.securityPolicy = securityPolicy; //設置超時 [_manager.requestSerializer willChangeValueForKey:@"timeoutinterval"]; _manager.requestSerializer.timeoutInterval = 20.f; [_manager.requestSerializer didChangeValueForKey:@"timeoutinterval"]; _manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringCacheData; _manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/xml",@"text/xml",@"text/plain",@"application/json",nil]; __weak typeof(self) weakSelf = self; [_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) { SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; /** * 導入多張CA證書 */ NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自簽名證書 NSData* caCert = [NSData dataWithContentsOfFile:cerPath]; NSArray *cerArray = @[caCert]; weakSelf.manager.securityPolicy.pinnedCertificates = cerArray; 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; } return disposition; }];
上述代碼通過給AFHTTPSessionManager重新設置證書驗證回調來自己驗證證書,然後將自己的證書加入到可信任的證書列表中,即可通過證書的校驗。
由於服務端使用.jks是一個證書庫,客戶端獲取到的證書可能不止一本,我這邊獲取到了兩本,具體獲取到基本可通過SecTrustGetCertificateCount方法獲取證書個數,AFNetworking在evaluateServerTrust:forDomain:方法中,AFSSLPinningMode的類型為AFSSLPinningModeCertificate和AFSSLPinningModePublicKey的時候都有校驗服務端的證書個數與客戶端信任的證書數量是否一樣,如果不一樣的話無法請求成功,所以這邊我就修改他的源碼,當有一個校驗成功時即算成功。
當類型為AFSSLPinningModeCertificate時
return trustedCertificateCount == [serverCertificates count] - 1;
為AFSSLPinningModePublicKey時
return trustedPublicKeyCount > 0 && ((self.validatesCertificateChain) || (!self.validatesCertificateChain && trustedPublicKeyCount >= 1));
去掉了第二塊中的trustedPublicKeyCount == [serverCertificates count]的條件。
這邊使用的AFNetworking的版本為2.5.3,如果其他版本有不同之處請自行根據實際情況修改。
demo地址:https://github.com/fengling2300/networkTest