本文用swift昧谊,實(shí)現(xiàn)在使用自簽名證書的情況下创倔,連接https服務(wù)器涯冠。Allow Arbitrary Loads設(shè)為NO磁滚,且無需把域名加入到NSExceptionDomains中。分別使用了:URLSession擦酌、 ASIHTTPRequest、 AFNetworking菠劝、NSURLConnection赊舶、RestKit、UIWebView。
swift版本
Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1)
Target: x86_64-apple-macosx10.9
蘋果毫無懸念地笼平,一直在反人類园骆。強(qiáng)制搞ATS這種事情,我覺得裝逼的成分多一點(diǎn)寓调,和蘋果一貫的作風(fēng)類似锌唾。https當(dāng)然比http要安全得多,但是讓如此眾多的廠商一齊搞這件事情夺英,太浪費(fèi)人力物力了晌涕。從泄密的嚴(yán)重程度來講,http根本不算什么重要原因痛悯。好萊塢女星的艷照就不是http的原因泄露的吧余黎?蘋果完全可以要求新上線的APP都用https,已經(jīng)上線的APP則可暫緩载萌。很多APP說不定過兩年就死掉了呢惧财?
本來上https也不難,但是受信證書是要花錢的扭仁。老板摳門不愿意買證書是一個(gè)方面垮衷,一個(gè)內(nèi)部API要額外花錢也有些不合理。所以本文就是幫你老板省錢的乖坠。
另一個(gè)好消息是搀突,本來2016年年底是最后期限,蘋果卻在2016年12月21日發(fā)了個(gè)文瓤帚,說期限拖延了描姚,拖延多久未知。
Supporting App Transport Security
看樣子是屈服于壓力妥協(xié)了戈次。
不過該來的總要來的轩勘,可以先把ATS搞起來,練練手怯邪。
證書
本篇不介紹證書的頒發(fā)及服務(wù)器的配置绊寻。
簡單講幾個(gè)注意點(diǎn)。
一悬秉、蘋果對于證書是有要求的澄步,在這里。具體看Requirements for Connecting Using ATS一節(jié)和泌。
請嚴(yán)格按說明配置證書村缸。
二、對于已配好的服務(wù)器武氓,可以用騰訊的這項(xiàng)服務(wù)檢測是否正常:蘋果ATS檢測
下圖是我的站的檢測結(jié)果梯皿,除了“證書被iOS9信任”這一條可以不通過以外仇箱,其他所有項(xiàng)必須通過檢測。
三东羹、Charles不要開剂桥。Charles證書沒配好的情況下,HTTPS是連不上的属提;配好的情況下权逗,程序沒寫對也能連得上。
基本思路
既然使用了https冤议,那么安全性還是要講究的斟薇。
程序的基本思路是先將證書添加到APP項(xiàng)目中,用SecTrustSetAnchorCertificates方法將其設(shè)置為信任求类,再用SecTrustEvaluate方法驗(yàn)證服務(wù)器的證書是否可信奔垦,最后生成憑證傳回服務(wù)器。
不過尸疆,如果你懶得驗(yàn)證證書椿猎,上述步驟也可以簡化,我會在URLSession一節(jié)中額外闡述一下寿弱。
URLSession
作為iOS新一代的網(wǎng)絡(luò)連接API犯眠,URLSession能很簡單地實(shí)現(xiàn)自簽名證書的HTTPS。我將它寫在第一位症革,希望讀者能仔細(xì)閱讀筐咧,學(xué)會基本原理。這樣對于本文沒有寫到的框架也能舉一反三噪矛,實(shí)現(xiàn)功能量蕊。
首先,我們需要把證書文件復(fù)制到項(xiàng)目中艇挨,并在Copy Bundle Resources里添加證書文件残炮。然后在程序中這樣讀取證書:
//導(dǎo)入客戶端證書
guard let cerPath = Bundle.main.path(forResource: "ca", ofType: "cer") else { return }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: cerPath)) else { return }
guard let certificate = SecCertificateCreateWithData(nil, data as CFData) else { return }
trustedCertList = [certificate]
要實(shí)現(xiàn)憑證回傳,必須使用異步調(diào)用缩滨,同步調(diào)用是沒戲的势就。
具體的我就不寫了,大致這樣就好:
let task = session.dataTask(with: request as URLRequest, completionHandler:{(data, response, error) -> () in
if error != nil {
return
}
let newStr = String(data: data!, encoding: .utf8)
print(newStr ?? "")
})
task.resume()
如果發(fā)送的請求是https的脉漏,URLSession會回調(diào)如下方法:(需聲明實(shí)現(xiàn)URLSessionTaskDelegate)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
在這個(gè)方法里苞冯,我們首先要把前面取到的trustedCertList設(shè)置為信任,接著要根據(jù)本地證書來驗(yàn)證服務(wù)器的證書是否可信侧巨,最后把憑證回傳舅锄。
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var err: OSStatus
var disposition: Foundation.URLSession.AuthChallengeDisposition = Foundation.URLSession.AuthChallengeDisposition.performDefaultHandling
var trustResult: SecTrustResultType = .invalid
var credential: URLCredential? = nil
//獲取服務(wù)器的trust object
let serverTrust: SecTrust = challenge.protectionSpace.serverTrust!
//將讀取的證書設(shè)置為serverTrust的根證書
err = SecTrustSetAnchorCertificates(serverTrust, trustedCertList)
if err == noErr {
//通過本地導(dǎo)入的證書來驗(yàn)證服務(wù)器的證書是否可信
err = SecTrustEvaluate(serverTrust, &trustResult)
}
if err == errSecSuccess && (trustResult == .proceed || trustResult == .unspecified) {
//認(rèn)證成功,則創(chuàng)建一個(gè)憑證返回給服務(wù)器
disposition = Foundation.URLSession.AuthChallengeDisposition.useCredential
credential = URLCredential(trust: serverTrust)
} else {
disposition = Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge
}
//回調(diào)憑證司忱,傳遞給服務(wù)器
completionHandler(disposition, credential)
//如果不論安全性巧娱,不想驗(yàn)證證書是否正確碉怔。那上面的代碼都不需要烘贴,直接寫下面這段即可
//let serverTrust: SecTrust = challenge.protectionSpace.serverTrust!
//SecTrustSetAnchorCertificates(serverTrust, trustedCertList)
//completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
最下面的三行被注釋掉的程序禁添,是無條件確認(rèn)服務(wù)器證書可信的。我不建議這樣做桨踪,上面的代碼寫寫也沒多少老翘。
如果出現(xiàn)
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
基本原因是SecTrustSetAnchorCertificates方法沒寫或沒寫對。
ASIHTTPRequest
這個(gè)庫特別古老锻离,用的人也不多铺峭,如果不是項(xiàng)目中用到了這個(gè),我是懶得寫它的汽纠。
這個(gè)庫還有很多坑卫键。
首先,它要用到的證書是p12格式的虱朵;
其次莉炉,它底層設(shè)置信任的代碼有問題,不但有內(nèi)存泄露碴犬,而且證書鏈也會出錯(cuò)絮宁。
先看一下swift部分的代碼,下面是發(fā)送請求的部分服协,接受的部分我就不寫了:
let url = URL(string: urlString)
let request = ASIHTTPRequest.request(with: url) as! ASIHTTPRequest
//導(dǎo)入客戶端證書
guard let cerPath = Bundle.main.path(forResource: "ca", ofType: "p12") else { return }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: cerPath)) else { return }
var identity: SecIdentity? = nil
if self.extractIdentity(outIdentity: &identity, cerData: data) {
//設(shè)置證書绍昂,這句話是關(guān)鍵
request.setClientCertificateIdentity(identity!)
request.delegate = self
request.startAsynchronous()
}
//如果不論安全性,不想驗(yàn)證證書是否正確偿荷。那上面的代碼都不需要窘游,直接寫下面這段即可
//request.validatesSecureCertificate = false
//request.delegate = self
//request.startAsynchronous()
func extractIdentity(outIdentity: inout SecIdentity?, cerData: Data) -> Bool {
var securityError = errSecSuccess
//這個(gè)字典里的value是證書密碼
let optionsDictionary: Dictionary<String, CFString>? = [kSecImportExportPassphrase as String: "" as CFString]
var items: CFArray? = nil
securityError = SecPKCS12Import(cerData as CFData, optionsDictionary as! CFDictionary, &items)
if securityError == 0 {
let myIdentityAndTrust = items as! NSArray as! [[String:AnyObject]]
outIdentity = myIdentityAndTrust[0][kSecImportItemIdentity as String] as! SecIdentity?
} else {
print(securityError)
return false
}
return true
}
然后我們打開ASIHTTPRequest.m,來做一些修改跳纳。
// Tell CFNetwork to use a client certificate
if (clientCertificateIdentity) {
//NSMutableDictionary *sslProperties = [NSMutableDictionary dictionaryWithCapacity:1]; //舊代碼賦值
//鳴謝:http://bewithme.iteye.com/blog/1999031
NSMutableDictionary *sslProperties = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates,
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot,
[NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain,
kCFNull,kCFStreamSSLPeerName,
nil];
NSMutableArray *certificates = [NSMutableArray arrayWithCapacity:[clientCertificates count]+1];
// The first object in the array is our SecIdentityRef
[certificates addObject:(id)clientCertificateIdentity];
// If we've added any additional certificates, add them too
for (id cert in clientCertificates) {
[certificates addObject:cert];
}
[sslProperties setObject:certificates forKey:(NSString *)kCFStreamSSLCertificates];
CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, sslProperties);
[sslProperties release]; //新代碼添加
}
我們需要更改兩處:一是sslProperties的賦值忍饰,二是需要釋放sslProperties。
如果不更改sslProperties的值棒旗,就會報(bào)如下錯(cuò)誤喘批。
CFNetwork SSLHandshake failed (-9807)
Error Domain=ASIHTTPRequestErrorDomain Code=1 "A connection failure occurred: SSL problem (Possible causes may include a bad/expired/self-signed certificate, clock set to wrong date)" UserInfo={NSLocalizedDescription=A connection failure occurred: SSL problem (Possible causes may include a bad/expired/self-signed certificate, clock set to wrong date), NSUnderlyingError=0x608000059470 {Error Domain=NSOSStatusErrorDomain Code=-9807 "(null)" UserInfo={_kCFStreamErrorCodeKey=-9807, _kCFStreamErrorDomainKey=3}}}
如果你在OC下仍然有內(nèi)存泄露,那么extractIdentity方法的寫法可以參考一下蘋果的這份官方文檔铣揉。
最后還有一個(gè)問題饶深,extractIdentity這個(gè)方法,每次調(diào)用的時(shí)候都吃CPU逛拱。這個(gè)暫時(shí)沒有找到解決方案敌厘,請依據(jù)自己APP的CPU使用情況來權(quán)衡是否需要驗(yàn)證證書。不驗(yàn)證證書的方法朽合,代碼里也有俱两。URLSession等方法就不會每次都驗(yàn)證證書饱狂,所以沒有這個(gè)問題。
AFNetworking
AFNetworking是對NSURLSession的封裝宪彩,畢竟是知名庫休讳,對自簽名證書很友好。幾句話就能簡單搞定尿孔。
guard let cerPath = Bundle.main.path(forResource: "ca", ofType: "cer") else { return }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: cerPath)) else { return }
var certSet: Set<Data> = []
certSet.insert(data)
let manager = AFHTTPSessionManager(baseURL: URL(string: urlString))
manager.responseSerializer = AFHTTPResponseSerializer()
//pinningMode設(shè)置為證書形式
manager.securityPolicy = AFSecurityPolicy.init(pinningMode: .certificate, withPinnedCertificates: certSet)
//allowInvalidCertificates必須設(shè)為true
manager.securityPolicy.allowInvalidCertificates = true
manager.securityPolicy.validatesDomainName = true
manager.get(urlString, parameters: nil,
progress: {(pro: Progress) -> () in
},
success: {(dataTask: URLSessionDataTask?, responseData: Any) -> () in
print(String(data: responseData as! Data, encoding: .utf8)!)
},
failure: {(dataTask: URLSessionDataTask?, error: Error) -> () in
print(error)
})
參考AFNetworking的源代碼俊柔,在URLSession的回調(diào)中,調(diào)用了AFSecurityPolicy的evaluateServerTrust方法活合。在這個(gè)方法里雏婶,要過兩次AFServerTrustIsValid,以驗(yàn)證證書白指。第一次代碼是這樣的:
if (self.SSLPinningMode == AFSSLPinningModeNone) {
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
為了不讓方法返回NO留晚,我們必須把a(bǔ)llowInvalidCertificates設(shè)置為true。在后面的代碼中告嘲,執(zhí)行過SecTrustSetAnchorCertificates了之后错维,AFServerTrustIsValid就會返回YES了。
NSURLConnection
這個(gè)東西將是明日黃花了状蜗,以后都應(yīng)該用URLSession的需五。
它的寫法與URLSession差不多,只在判斷證書是否正確的地方有些修改轧坎。
func connection(_ connection: NSURLConnection, willSendRequestFor challenge: URLAuthenticationChallenge) {
var trustResult: SecTrustResultType = .invalid
let serverTrust: SecTrust = challenge.protectionSpace.serverTrust!
var err: OSStatus = SecTrustSetAnchorCertificates(serverTrust, trustedCertList)
if err == noErr {
//通過本地導(dǎo)入的證書來驗(yàn)證服務(wù)器的證書是否可信
err = SecTrustEvaluate(serverTrust, &trustResult)
}
if err == errSecSuccess && (trustResult == .proceed || trustResult == .unspecified) {
//認(rèn)證成功宏邮,則創(chuàng)建一個(gè)憑證返回給服務(wù)器
challenge.sender?.use(URLCredential(trust: serverTrust), for: challenge)
challenge.sender?.continueWithoutCredential(for: challenge)
} else {
challenge.sender?.cancel(challenge)
}
//如果不論安全性,不想驗(yàn)證證書是否正確缸血。那上面的代碼都不需要蜜氨,直接寫下面這段即可
//let serverTrust: SecTrust = challenge.protectionSpace.serverTrust!
//SecTrustSetAnchorCertificates(serverTrust, trustedCertList)
//challenge.sender?.use(URLCredential(trust: serverTrust), for: challenge)
//challenge.sender?.continueWithoutCredential(for: challenge)
}
RestKit
又一個(gè)古老久遠(yuǎn)不好用的框架。我也是蠻佩服人人網(wǎng)當(dāng)時(shí)的架構(gòu)師的捎泻,選的框架都是奇葩飒炎。
這個(gè)破爛框架一樣需要修改底層代碼,不改就會報(bào)如下錯(cuò)誤:
Error Domain=NSURLErrorDomain Code=-1012 "(null)"
關(guān)鍵的改動(dòng)是RestKit/Network/AFNetworking/AFRKURLConnectionOperation.m的willSendRequestForAuthenticationChallenge方法
在case AFRKSSLPinningModeCertificate處笆豁,這里的驗(yàn)證是有問題的郎汪,原代碼如下:
case AFRKSSLPinningModeCertificate: {
NSAssert([[self.class pinnedCertificates] count] > 0, @"AFRKSSLPinningModeCertificate needs at least one certificate file in the application bundle");
for (id serverCertificateData in trustChain) {
if ([[self.class pinnedCertificates] containsObject:serverCertificateData]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
return;
}
}
NSLog(@"Error: Unknown Certificate during Pinning operation");
[[challenge sender] cancelAuthenticationChallenge:challenge];
break;
}
改過以后的代碼如下:
case AFRKSSLPinningModeCertificate: {
NSAssert([[self.class pinnedCertificates] count] > 0, @"AFRKSSLPinningModeCertificate needs at least one certificate file in the application bundle");
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in [self.class pinnedCertificates]) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (AFServerTrustIsValid(serverTrust)) {
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([[self.class pinnedCertificates] containsObject:trustChainCertificate]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
return;
}
}
}
//這段代碼完全錯(cuò)誤,for里面的if語句不可能為true
//for (id serverCertificateData in trustChain) {
// if ([[self.class pinnedCertificates] containsObject:serverCertificateData]) {
// NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
// [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
// return;
// }
//}
NSLog(@"Error: Unknown Certificate during Pinning operation");
[[challenge sender] cancelAuthenticationChallenge:challenge];
break;
}
這里關(guān)鍵一句是SecTrustSetAnchorCertificates闯狱。這句話將pinnedCertificates里面的證書設(shè)置為信任(pinnedCertificates里面的證書是在初始化對象的時(shí)候從資源文件里取的*.cer文件)煞赢。原來的代碼沒有這句話,所以if ([[self.class pinnedCertificates] containsObject:serverCertificateData])肯定無法驗(yàn)證自己的證書哄孤,于是返回false照筑。
相關(guān)函數(shù)追加:
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
[trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
}
return [NSArray arrayWithArray:trustChain];
}
最后,還需要添加一個(gè)文件頭:
#import <AssertMacros.h>
最后,在發(fā)送請求的時(shí)候凝危,需要設(shè)置一下pinningMode波俄。
let httpClient = AFRKHTTPClient.init(baseURL: URL(string: urlString))
let manager = RKObjectManager.init(httpClient: httpClient)
manager?.httpClient.defaultSSLPinningMode = AFRKSSLPinningModeCertificate
UIWebView
App Transport Security Settings下面除了有Allow Arbitrary Loads還有一個(gè)屬性Allow Arbitrary Loads in Web Content。只要把這個(gè)屬性設(shè)置為YES蛾默,UIWebView就可以訪問http的頁面了懦铺。不過,我不知道這個(gè)屬性設(shè)置為YES趴生,到時(shí)候APP會不會被蘋果拒絕阀趴。
如果你仔細(xì)閱讀了上面的各種方法,那要讓UIWebView支持自簽名證書苍匆,就很簡單了∨锞眨基本思路就是用URLSession來獲取頁面文本浸踩,然后調(diào)用loadHTMLString來顯示。具體代碼我就不貼了统求,需要的話可以到文末去下載源代碼检碗。
代碼
源代碼在這里。如果你用的框架沒有包含在這里也不要緊码邻,看明白上面的例子就一定能融會貫通折剃。