在 iOS 中進(jìn)行網(wǎng)絡(luò)通信時(shí)捌治,為了安全,可能會(huì)產(chǎn)生認(rèn)證質(zhì)詢(Authentication Challenge)
場(chǎng)景
- 當(dāng)遠(yuǎn)程服務(wù)器要求客戶證書或 Windows NT LAN Manager (NTLM) 驗(yàn)證時(shí),允許您的應(yīng)用程序提供適當(dāng)?shù)膽{證填具。
- 當(dāng)一個(gè)會(huì)話首次建立與使用
SSL
或TLS
的遠(yuǎn)程服務(wù)器的連接時(shí)统舀,為了讓你的應(yīng)用程序驗(yàn)證服務(wù)器的證書鏈匆骗。
接收質(zhì)詢
在代碼需要向認(rèn)證的服務(wù)器請(qǐng)求資源時(shí),服務(wù)器會(huì)使用 http 狀態(tài)碼 401 進(jìn)行響應(yīng)誉简,即訪問被拒絕需要驗(yàn)證碉就。URLSession 會(huì)接收到響應(yīng)并在對(duì)應(yīng)的代理方法中處理質(zhì)詢。過程如下所示:
質(zhì)詢類型對(duì)應(yīng)的處理方法
session-level 代理方法
它是 URLSession
的代理方法
optional func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
non-session-level 代理方法
它是 URLSessionTask
的代理方法
optional func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
代理參數(shù)詳解
- session: URLSession
->
當(dāng)前的會(huì)話對(duì)象 - task: URLSessionTask
->
當(dāng)前的任務(wù)對(duì)象 - challenge: URLAuthenticationChallenge
->
包含認(rèn)證請(qǐng)求的對(duì)象 - completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
->
處理完質(zhì)詢之后需要調(diào)用的回調(diào)- URLSession.AuthChallengeDisposition
->
如何處理質(zhì)詢 - URLCredential
->
對(duì)應(yīng)質(zhì)詢類型的認(rèn)證憑證
- URLSession.AuthChallengeDisposition
注意:
- 如果沒有實(shí)現(xiàn) URLSession 或者 URLSessionTask 的代理方法來正確的響應(yīng)挑戰(zhàn)闷串,那么就會(huì)收到 401(禁止)錯(cuò)誤瓮钥。
- 如果沒有實(shí)現(xiàn) URLSession 的代理方法,session-level 的質(zhì)詢會(huì)走 URLSessionTask 的代理來處理烹吵,而 task-level 的質(zhì)詢不會(huì)通過 URLSession 的代理方法碉熄。
認(rèn)識(shí) URLAuthenticationChallenge、URLProtectionSpace肋拔、URLCredential锈津、URLSession.AuthChallengeDisposition
URLAuthenticationChallenge
class URLAuthenticationChallenge: NSObject {
// 需要認(rèn)證的區(qū)域
var protectionSpace: URLProtectionSpace
// 表示最后一次認(rèn)證失敗的 URLResponse 實(shí)例
var failureResponse: URLResponse?
// 之前認(rèn)證失敗的次數(shù)
var previousFailureCount: Int
// 建議的憑據(jù),有可能是質(zhì)詢提供的默認(rèn)憑據(jù)凉蜂,也有可能是上次認(rèn)證失敗時(shí)使用的憑據(jù)
var proposedCredential: URLCredential?
// 上次認(rèn)證失敗的 Error 實(shí)例
var error: Error?
// 質(zhì)詢的發(fā)送者
var sender: URLAuthenticationChallengeSender?
}
URLProtectionSpace
質(zhì)詢類型等各種信息都在 URLProtectionSpace
對(duì)象中
authenticationMethod
的值表示了質(zhì)詢的類型琼梆,根據(jù)這個(gè)值來決定我們?cè)趺错憫?yīng)挑戰(zhàn),具體類型見上文窿吩。
class URLProtectionSpace : NSObject {
// 質(zhì)詢的類型
var authenticationMethod: String
// 進(jìn)行客戶端證書認(rèn)證時(shí)茎杂,可接受的證書頒發(fā)機(jī)構(gòu)
var distinguishedNames: [Data]?
var host: String
var port: Int
var `protocol`: String?
var proxyType: String?
var realm: String?
var receivesCredentialSecurely: Bool
// 表示服務(wù)器的SSL事務(wù)狀態(tài)
var serverTrust: SecTrust?
}
URLCredential
成功響應(yīng)質(zhì)詢,還需要提供對(duì)應(yīng)的憑據(jù)纫雁。有三種初始化方式煌往,分別用于不同類型的質(zhì)詢類型。
// 使用給定的持久性設(shè)置先较、用戶名和密碼創(chuàng)建 URLCredential 實(shí)例携冤。
public init(user: String, password: String, persistence: URLCredential.Persistence) {
}
// 用于客戶端證書認(rèn)證質(zhì)詢,當(dāng) challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate 時(shí)使用
// identity: 私鑰和和證書的組合
// certArray: 大多數(shù)情況下傳 nil
// persistence: 該參數(shù)會(huì)被忽略闲勺,傳 .forSession 會(huì)比較合適
public init(identity: SecIdentity, certificates certArray: [Any]?, persistence: URLCredential.Persistence) {
}
// 用于服務(wù)器信任認(rèn)證質(zhì)詢曾棕,當(dāng) challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust 時(shí)使用
// 從 challenge.protectionSpace.serverTrust 中獲取 SecTrust 實(shí)例
// 使用該方法初始化 URLCredential 實(shí)例之前,需要對(duì) SecTrust 實(shí)例進(jìn)行評(píng)估
public init(trust: SecTrust) {
}
URLCredential.Persistence
用于表明 URLCredential 實(shí)例的持久化方式菜循,只有基于用戶名和密碼創(chuàng)建的 URLCredential 實(shí)例才會(huì)被持久化到 keychain 里面
public enum Persistence : UInt {
case none
case forSession
// 會(huì)存儲(chǔ)在 iOS 的 keychain 里面
case permanent
// 會(huì)存儲(chǔ)在 iOS 的 keychain 里面翘地,并且會(huì)通過 iCloud 同步到其他 iOS 設(shè)備
@available(iOS 6.0, *)
case synchronizable
}
URLSession.AuthChallengeDisposition
public enum AuthChallengeDisposition : Int {
// 使用指定的憑據(jù)(credential)
case useCredential
// 默認(rèn)的質(zhì)詢處理,如果有提供憑據(jù)也會(huì)被忽略癌幕,如果沒有實(shí)現(xiàn) URLSessionDelegate 處理質(zhì)詢的方法則會(huì)使用這種方式
case performDefaultHandling
// 取消認(rèn)證質(zhì)詢衙耕,如果有提供憑據(jù)也會(huì)被忽略,會(huì)取消當(dāng)前的 URLSessionTask 請(qǐng)求
case cancelAuthenticationChallenge
// 拒絕質(zhì)詢勺远,并且進(jìn)行下一個(gè)認(rèn)證質(zhì)詢橙喘,如果有提供憑據(jù)也會(huì)被忽略;大多數(shù)情況不會(huì)使用這種方式胶逢,無法為某個(gè)質(zhì)詢提供憑據(jù)厅瞎,則通常應(yīng)返回 performDefaultHandling
case rejectProtectionSpace
}
如何響應(yīng)質(zhì)詢
兩個(gè)接收質(zhì)詢的代理方法都有 session, challenge, 以及一個(gè) completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 閉包參數(shù)饰潜。
這個(gè)閉包接受兩個(gè)參數(shù),它們的類型分別為 URLSession.AuthChallengeDisposition 和簸、 URLCredential? 彭雾,需要根據(jù) challenge.protectionSpace.authenticationMethod 的值,確定如何響應(yīng)質(zhì)詢锁保,并且提供對(duì)應(yīng)的 URLCredential 實(shí)例
注意:
如果實(shí)現(xiàn)了兩個(gè)代理方法薯酝,執(zhí)行完自己的認(rèn)證邏輯之后,必須調(diào)用這個(gè)閉包來響應(yīng)質(zhì)詢爽柒,否則 NSURLSessionTask 會(huì)一直等待吴菠,既不會(huì)成功也不會(huì)失敗。
1 non-session-level
1.1 HTTP Basic
客戶端 -> 發(fā)送請(qǐng)求
服務(wù)器 -> 返回狀態(tài)碼 401 告訴客戶端需要認(rèn)證
客戶端 -> 用戶名和密碼 Base64 方式編碼后發(fā)送
服務(wù)器 -> 認(rèn)證成功返回 200霉赡,否則 401
1.2 HTTP Digest
客戶端 -> 發(fā)送請(qǐng)求
服務(wù)器 -> 返回狀態(tài)碼 401 及臨時(shí)的質(zhì)詢碼(隨機(jī)數(shù))
客戶端 -> 發(fā)送摘要以及由質(zhì)詢碼計(jì)算出的響應(yīng)碼
服務(wù)器 -> 認(rèn)證成功返回 200橄务,否則 401
1.3 HTMLForm
網(wǎng)上找的資料說,URLSession 不會(huì)觸發(fā)此類質(zhì)詢
1.4 iOS 實(shí)際代碼中如何處理
HTTP Basic
穴亏、 HTTP Digest
蜂挪、 NTLM
都是基于用戶名/密碼的認(rèn)證,處理這種認(rèn)證質(zhì)詢的
NTLM 屬于 session-level嗓化,Negotiate 實(shí)際上也是 NTLM棠涮,寫在這里方便大家閱讀
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate:
let user = "user"
let password = "password"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
completionHandler(.useCredential, credential)
default:
completionHandler(.performDefaultHandling, nil)
}
}
2 session-level
2.1 NSURLAuthenticationMethodClientCertificate
略
2.2 HTTPS Server Trust Authentication
大多數(shù)情況下,對(duì)于這種類型的認(rèn)證質(zhì)詢可以不實(shí)現(xiàn) URLSessionDelegate 處理認(rèn)證質(zhì)詢的方法刺覆, URLSessionTask 會(huì)使用默認(rèn)的處理方式( performDefaultHandling )進(jìn)行處理严肪。但是如果是以下的情況,則需要手動(dòng)進(jìn)行處理:
- 與使用自簽名證書的服務(wù)器進(jìn)行 HTTPS 連接谦屑。
- 進(jìn)行更嚴(yán)格的服務(wù)器信任評(píng)估來加強(qiáng)安全性驳糯,如:通過使用 SSL Pinning 來防止中間人攻擊。
2.2.1 處理權(quán)威機(jī)構(gòu)簽發(fā)的證書
對(duì)于權(quán)威機(jī)構(gòu)簽發(fā)的證書, 這類證書上面會(huì)聲明自己是由哪一個(gè)CA機(jī)構(gòu)(或CA的子機(jī)構(gòu))簽發(fā), 而對(duì)應(yīng)的CA機(jī)構(gòu)也有自己的CA證書, 在手機(jī)出廠之前就被安裝進(jìn)系統(tǒng)里了, 這樣對(duì)于權(quán)威機(jī)構(gòu)簽發(fā)的服務(wù)器證書, 只要從系統(tǒng)里找一下服務(wù)器證書對(duì)應(yīng)的CA證書, 拿CA證書的公鑰解密一下服務(wù)器證書的簽名, 解密出的Hash是不是和服務(wù)器攜帶的數(shù)據(jù)部分運(yùn)算出的Hash一致, 即可證明服務(wù)器證書是合法的. 如果不實(shí)現(xiàn)didReceiveChallenge這個(gè)協(xié)議方法, 系統(tǒng)會(huì)自動(dòng)幫忙處理好.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// 判斷認(rèn)證質(zhì)詢的類型氢橙,判斷是否存在服務(wù)器信任實(shí)例 serverTrust
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
// 否則使用默認(rèn)處理
completionHandler(.performDefaultHandling, nil)
return
}
// 自定義方法酝枢,對(duì)服務(wù)器信任實(shí)例 serverTrust 進(jìn)行評(píng)估
if evaluate(trust, forHost: challenge.protectionSpace.host) {
// 評(píng)估通過則創(chuàng)建 URLCredential 實(shí)例,告訴系統(tǒng)接受服務(wù)器的憑據(jù)
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// 否則取消這次認(rèn)證悍手,告訴系統(tǒng)拒絕服務(wù)器的憑據(jù)
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
func evaluate(serverTrust: SecTrust, forHost: String) -> Bool {
var trust : Bool = false
if #available(iOS 12, *) {
var error: CFError?
trust = SecTrustEvaluateWithError(serverTrust, &error)
} else {
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &result)
trust = (status == errSecSuccess && (result == .unspecified || result == .proceed))
}
return trust
}
2.2.2 自簽名證書
比如 charles 或者各種抓包軟件帘睦,實(shí)際上他們就是自簽證書,
自簽名的證書是過不了系統(tǒng)的證書驗(yàn)證的坦康,如果服務(wù)器用了自簽名證書竣付,還想正常的訪問的話,需要把自簽證書添加到鑰匙串并信任滞欠,或者做自簽名證書的客戶端驗(yàn)證