在 iOS 中进行网络通信时,为了安全,可能会产生认证质询(Authentication Challenge)
场景
- 当远程服务器要求客户证书或 Windows NT LAN Manager (NTLM) 验证时,允许您的应用程序提供适当的凭证。
- 当一个会话首次建立与使用
SSL
或TLS
的远程服务器的连接时,为了让你的应用程序验证服务器的证书链。
接收质询
在代码需要向认证的服务器请求资源时,服务器会使用 http 状态码 401 进行响应,即访问被拒绝需要验证。URLSession 会接收到响应并在对应的代理方法中处理质询。过程如下所示:
质询类型对应的处理方法
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)
代理参数详解
- session: URLSession
->
当前的会话对象 - task: URLSessionTask
->
当前的任务对象 - challenge: URLAuthenticationChallenge
->
包含认证请求的对象 - completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
->
处理完质询之后需要调用的回调- URLSession.AuthChallengeDisposition
->
如何处理质询 - URLCredential
->
对应质询类型的认证凭证
- URLSession.AuthChallengeDisposition
注意:
- 如果没有实现 URLSession 或者 URLSessionTask 的代理方法来正确的响应挑战,那么就会收到 401(禁止)错误。
- 如果没有实现 URLSession 的代理方法,session-level 的质询会走 URLSessionTask 的代理来处理,而 task-level 的质询不会通过 URLSession 的代理方法。
认识 URLAuthenticationChallenge、URLProtectionSpace、URLCredential、URLSession.AuthChallengeDisposition
URLAuthenticationChallenge
class URLAuthenticationChallenge: NSObject {
// 需要认证的区域
var protectionSpace: URLProtectionSpace
// 表示最后一次认证失败的 URLResponse 实例
var failureResponse: URLResponse?
// 之前认证失败的次数
var previousFailureCount: Int
// 建议的凭据,有可能是质询提供的默认凭据,也有可能是上次认证失败时使用的凭据
var proposedCredential: URLCredential?
// 上次认证失败的 Error 实例
var error: Error?
// 质询的发送者
var sender: URLAuthenticationChallengeSender?
}
URLProtectionSpace
质询类型等各种信息都在 URLProtectionSpace
对象中
authenticationMethod
的值表示了质询的类型,根据这个值来决定我们怎么响应挑战,具体类型见上文。
class URLProtectionSpace : NSObject {
// 质询的类型
var authenticationMethod: String
// 进行客户端证书认证时,可接受的证书颁发机构
var distinguishedNames: [Data]?
var host: String
var port: Int
var `protocol`: String?
var proxyType: String?
var realm: String?
var receivesCredentialSecurely: Bool
// 表示服务器的SSL事务状态
var serverTrust: SecTrust?
}
URLCredential
成功响应质询,还需要提供对应的凭据。有三种初始化方式,分别用于不同类型的质询类型。
// 使用给定的持久性设置、用户名和密码创建 URLCredential 实例。
public init(user: String, password: String, persistence: URLCredential.Persistence) {
}
// 用于客户端证书认证质询,当 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate 时使用
// identity: 私钥和和证书的组合
// certArray: 大多数情况下传 nil
// persistence: 该参数会被忽略,传 .forSession 会比较合适
public init(identity: SecIdentity, certificates certArray: [Any]?, persistence: URLCredential.Persistence) {
}
// 用于服务器信任认证质询,当 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust 时使用
// 从 challenge.protectionSpace.serverTrust 中获取 SecTrust 实例
// 使用该方法初始化 URLCredential 实例之前,需要对 SecTrust 实例进行评估
public init(trust: SecTrust) {
}
URLCredential.Persistence
用于表明 URLCredential 实例的持久化方式,只有基于用户名和密码创建的 URLCredential 实例才会被持久化到 keychain 里面
public enum Persistence : UInt {
case none
case forSession
// 会存储在 iOS 的 keychain 里面
case permanent
// 会存储在 iOS 的 keychain 里面,并且会通过 iCloud 同步到其他 iOS 设备
@available(iOS 6.0, *)
case synchronizable
}
URLSession.AuthChallengeDisposition
public enum AuthChallengeDisposition : Int {
// 使用指定的凭据(credential)
case useCredential
// 默认的质询处理,如果有提供凭据也会被忽略,如果没有实现 URLSessionDelegate 处理质询的方法则会使用这种方式
case performDefaultHandling
// 取消认证质询,如果有提供凭据也会被忽略,会取消当前的 URLSessionTask 请求
case cancelAuthenticationChallenge
// 拒绝质询,并且进行下一个认证质询,如果有提供凭据也会被忽略;大多数情况不会使用这种方式,无法为某个质询提供凭据,则通常应返回 performDefaultHandling
case rejectProtectionSpace
}
如何响应质询
两个接收质询的代理方法都有 session, challenge, 以及一个 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 闭包参数。
这个闭包接受两个参数,它们的类型分别为 URLSession.AuthChallengeDisposition 、 URLCredential? ,需要根据 challenge.protectionSpace.authenticationMethod 的值,确定如何响应质询,并且提供对应的 URLCredential 实例
注意:
如果实现了两个代理方法,执行完自己的认证逻辑之后,必须调用这个闭包来响应质询,否则 NSURLSessionTask 会一直等待,既不会成功也不会失败。
1 non-session-level
1.1 HTTP Basic
客户端 -> 发送请求
服务器 -> 返回状态码 401 告诉客户端需要认证
客户端 -> 用户名和密码 Base64 方式编码后发送
服务器 -> 认证成功返回 200,否则 401
1.2 HTTP Digest
客户端 -> 发送请求
服务器 -> 返回状态码 401 及临时的质询码(随机数)
客户端 -> 发送摘要以及由质询码计算出的响应码
服务器 -> 认证成功返回 200,否则 401
1.3 HTMLForm
网上找的资料说,URLSession 不会触发此类质询
1.4 iOS 实际代码中如何处理
HTTP Basic
、 HTTP Digest
、 NTLM
都是基于用户名/密码的认证,处理这种认证质询的
NTLM 属于 session-level,Negotiate 实际上也是 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
大多数情况下,对于这种类型的认证质询可以不实现 URLSessionDelegate 处理认证质询的方法, URLSessionTask 会使用默认的处理方式( performDefaultHandling )进行处理。但是如果是以下的情况,则需要手动进行处理:
- 与使用自签名证书的服务器进行 HTTPS 连接。
- 进行更严格的服务器信任评估来加强安全性,如:通过使用 SSL Pinning 来防止中间人攻击。
2.2.1 处理权威机构签发的证书
对于权威机构签发的证书, 这类证书上面会声明自己是由哪一个CA机构(或CA的子机构)签发, 而对应的CA机构也有自己的CA证书, 在手机出厂之前就被安装进系统里了, 这样对于权威机构签发的服务器证书, 只要从系统里找一下服务器证书对应的CA证书, 拿CA证书的公钥解密一下服务器证书的签名, 解密出的Hash是不是和服务器携带的数据部分运算出的Hash一致, 即可证明服务器证书是合法的. 如果不实现didReceiveChallenge这个协议方法, 系统会自动帮忙处理好.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// 判断认证质询的类型,判断是否存在服务器信任实例 serverTrust
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
// 否则使用默认处理
completionHandler(.performDefaultHandling, nil)
return
}
// 自定义方法,对服务器信任实例 serverTrust 进行评估
if evaluate(trust, forHost: challenge.protectionSpace.host) {
// 评估通过则创建 URLCredential 实例,告诉系统接受服务器的凭据
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// 否则取消这次认证,告诉系统拒绝服务器的凭据
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 或者各种抓包软件,实际上他们就是自签证书,
自签名的证书是过不了系统的证书验证的,如果服务器用了自签名证书,还想正常的访问的话,需要把自签证书添加到钥匙串并信任,或者做自签名证书的客户端验证