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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容