iOS 质询机制Authentication Challenge

在 iOS 中进行网络通信时,为了安全,可能会产生认证质询(Authentication Challenge)

场景

  • 当远程服务器要求客户证书或 Windows NT LAN Manager (NTLM) 验证时,允许您的应用程序提供适当的凭证。
  • 当一个会话首次建立与使用 SSLTLS 的远程服务器的连接时,为了让你的应用程序验证服务器的证书链。

接收质询

在代码需要向认证的服务器请求资源时,服务器会使用 http 状态码 401 进行响应,即访问被拒绝需要验证。URLSession 会接收到响应并在对应的代理方法中处理质询。过程如下所示:


111.png

质询类型对应的处理方法

222.png

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 或者 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 BasicHTTP DigestNTLM 都是基于用户名/密码的认证,处理这种认证质询的

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 或者各种抓包软件,实际上他们就是自签证书,
自签名的证书是过不了系统的证书验证的,如果服务器用了自签名证书,还想正常的访问的话,需要把自签证书添加到钥匙串并信任,或者做自签名证书的客户端验证

你可能感兴趣的:(iOS 质询机制Authentication Challenge)