NSURLSession 與 NSURLAuthenticationMethodServerTrust
—— 從零開始的“服務器信任質詢”全流程
目標讀者:剛接觸 iOS 網絡開發、準備理解 HTTPS 與證書校驗細節的同學
出發點:搞清楚為什么會有“質詢”、質詢的觸發時機、以及在 delegate 里怎么正確地處理它。
1. 質詢到底是什么?
當 URLSession
發現需要某種額外憑據(credential)才能繼續網絡交互時,會暫停請求并向你拋出 authentication challenge。對 HTTPS 來說,最常見的觸發類型就是 NSURLAuthenticationMethodServerTrust
:
-
服務器把 X.509 證書鏈塞進 TLS 握手。
-
客戶端(iOS TLS 實現 + ATS 默認策略)檢查:
- 證書是否在有效期、是否被吊銷;
- 證書鏈是否能追溯到系統或配置的受信根 CA;
- 證書的 CN/SAN 是否與請求的 host 完全匹配。
-
如果 全部 檢查都能自動通過,
URLSession
不會打擾你——直接走默認證書校驗并繼續請求。 -
只要 你實現了 session-level delegate 方法
urlSession(_:didReceive:completionHandler:)
,系統就會把步驟 2 的工作“交卷”給你——即使校驗本來能自動通過。
🚩 所以:不實現該 delegate == 自動信任系統 CA + ATS 默認策略;實現 delegate == 你必須親自裁定是否信任。
2. 質詢出現的典型場景
場景 | 為什么會收到質詢? | 你通常怎么做? |
---|---|---|
生產環境,使用合法證書(Let’s Encrypt、GlobalSign…) | 你自己實現了 delegate,但只是想保留系統默認驗證 | 再次調用 SecTrustEvaluateWithError ,通過則 .useCredential |
內網/測試環境 使用自簽名證書 | 系統根證書鏈里找不到頒發者 | 把自簽根證書預裝到 App Bundle 并做自定義信任 |
做 SSL Pinning(證書/公鑰固定) | 你想縮短信任鏈,拒絕被“合法”但非預期的 CA 篡改 | 手動比對二進制證書或公鑰哈希,然后再決定是否信任 |
使用 HTTP 抓包工具 (Charles、mitmproxy) | 代理偽造服務器證書,除非你安裝其證書為根 CA | 開發調試時允許 Charles 證書;上線包一定要拒絕 |
3. 基礎實現(Swift 5+)
/// 在創建 URLSession 時指定 delegate,而不是用 URLSession.shared
let session = URLSession(configuration: .default,delegate: self,delegateQueue: nil)extension YourNetworkManager: URLSessionDelegate {/// 系統對“服務器信任”發起的質詢都會走到這里func urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,let serverTrust = challenge.protectionSpace.serverTrust else {// 交給系統默認處理(例如 HTTP Basic、客戶端證書等)completionHandler(.performDefaultHandling, nil)return}// 1?? 讓系統再跑一次標準評估if SecTrustEvaluateWithError(serverTrust, nil) {let credential = URLCredential(trust: serverTrust)completionHandler(.useCredential, credential) // 繼續請求} else {completionHandler(.cancelAuthenticationChallenge, nil) // 終止}}
}
iOS 13- 及更早版本用
SecTrustEvaluate
;iOS 13+ 強烈建議改用SecTrustEvaluateWithError
以拿到CFError
信息并避免阻塞 main thread。
Objective-C 版本(簡化)
- (void)URLSession:(NSURLSession *)sessiondidReceiveChallenge:(NSURLAuthenticationChallenge *)challengecompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {SecTrustRef trust = challenge.protectionSpace.serverTrust;if (SecTrustEvaluateWithError(trust, NULL)) {NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];completionHandler(NSURLSessionAuthChallengeUseCredential, cred);} else {completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);}return;}completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
4. 深入:自簽名證書與 SSL Pinning
4.1 只信任 Bundle 中的根證書
if let certPath = Bundle.main.path(forResource: "myRootCA", ofType: "cer"),let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)),let rootCert = SecCertificateCreateWithData(nil, certData as CFData) {SecTrustSetAnchorCertificates(serverTrust, [rootCert] as CFArray)SecTrustSetAnchorCertificatesOnly(serverTrust, true)if SecTrustEvaluateWithError(serverTrust, nil) {completionHandler(.useCredential, URLCredential(trust: serverTrust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}
}
SecTrustSetAnchorCertificatesOnly
=true 的效果是“把系統根 CA 全部踢出,僅信任我給定的這一束證書”。
4.2 公鑰 Pinning(效率更高,證書續期更靈活)
guard let serverTrust = challenge.protectionSpace.serverTrust else { ... }
guard SecTrustEvaluateWithError(serverTrust, nil) else { ... }let serverPublicKey = SecTrustCopyKey(serverTrust)!
let serverKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil)! as Data
let serverKeyHash = SHA256(serverKeyData) // 自己寫或 CryptoKitif pinnedHashes.contains(serverKeyHash) {completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {completionHandler(.cancelAuthenticationChallenge, nil)
}
5. 典型錯誤排查清單
現象 | 根因 | 快速定位 |
---|---|---|
Code -999 “已取消” | 你在 delegate 里返回了 .cancelAuthenticationChallenge 或 .rejectProtectionSpace | 打斷點檢查 challenge.protectionSpace |
Code -1200 “SSL error” | 證書鏈無效 / ATS 阻止弱加密 | 觀察 Console,中會打印 ATS policy requires... |
Charles 無法抓包 | ATS 拒絕了 Charles 證書;或你啟用了 Pinning | 臨時將 NSExceptionDomains 加入 Info.plist,或關閉 Pinning |
偶發 ServerTrust 失敗 | 服務器有多個證書鏈、SNI/host 不一致 | 手動訪問 https://host 用 openssl s_client -servername host -connect ip:443 |
6. 最佳實踐速覽
- 不做弱校驗。切勿直接
.useCredential
而不跑SecTrustEvaluateWithError
,那等同于“信任一切”,上線會被審核拒絕。 - Pin 公鑰而非整張證書,減少因證書續期頻繁發版。
- 按需配置 ATS。絕大多數生產 HTTPS 服務都可以滿足 ATS 默認要求:TLS 1.2+、至少 RSA 2048 或 ECC 256 、SHA-256 簽名。
- 調試與上線嚴格隔離。把抓包例外、測試根證書全部寫在
#if DEBUG ... #endif
分支中。
結語
NSURLAuthenticationMethodServerTrust
看似只是“系統多問一句,你到底信不信任這臺服務器?”,但背后承載的是 PKI、TLS 乃至你 App 用戶的數據安全。真正的安全措施都在“默認正確”與“最小權限”:
默認讓系統校驗一切,只有當你非常確定要改時才介入,并且介入后要保證比系統 更嚴格 而不是更松。
掌握這些基礎,你就能輕松向自簽環境、抓包調試甚至 SSL Pinning 過渡,也能對任何“為什么連接被取消?”作出快速診斷。愿你寫出的每一行網絡代碼都能經得起安全審計與真實攻擊的考驗。