解决「 HTTPDNS + HTTPS 」的证书校验问题
Posted 知识小集
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决「 HTTPDNS + HTTPS 」的证书校验问题相关的知识,希望对你有一定的参考价值。
为了提升 App 网络请求的稳定可靠性,从不同维度考虑有很多的优化方案,今天我们就从“域名解析”切入来讲一讲。
下面先来介绍一下 HTTPDNS
服务,以及接入 HTTPDNS
后对 App 中原有的 HTTPS
请求的证书校验带来的影响和相关解决方案。
HTTPDNS
但在移动互联网络中,我们经常会遭遇到运营商的 DNS 劫持
(利益使然),导致 Web 页面出现弹窗、小广告、服务不稳定、不可用等。为了应对这种情况,很多云服务厂商都提供了 HTTPDNS
服务:
阿里云:https://www.aliyun.com/product/httpdns?spm=5176.10695662.765261.732.16cf5279THiIA4
腾讯云:https://cloud.tencent.com/product/hd?from=qcloudHpHeaderHd
HTTPDNS
使用HTTP
协议进行域名解析,代替现有基于UDP
的DNS
协议,域名解析请求直接发送到云服务商的HTTPDNS
服务器,从而绕过运营商的Local DNS
,能够有效避免Local DNS
造成的域名劫持、调度不精准、解析延迟、失败率高、不稳定等问题。 —— 引自阿里云文档
HTTPDNS
的基本原理如下图所示:
问题
当客户端使用 HTTPDNS
解析域名时,请求 URL
中的 host
会被替换成 HTTPDNS
解析出来的 IP
,这种方案对于 HTTP
请求不会有任何影响,但是对于 HTTPS
来说,由于请求前多了一个 SSL/TLS
握手过程,涉及到证书校验,这时候问题就来了!
在 SSL/TLS
握手过程中,服务端下发的证书里的 CN
字段(即证书颁发的域名)仍然为域名的形式,但是请求中的 host
在请求前已经被我们替换为 IP
了,这时在证书校验时,就会出现 domain
不匹配的情况,导致 SSL/TLS
握手不成功,请求会被取消掉(error code: -999
)。
解决方法
因此,我们需要对证书校验的逻辑做一下小改动,在 NSURLSession
的证书校验代理方法(URLSession:didReceiveChallenge:completionHandler:
)中,增加一个前置处理:把待验证的 domian
由原本的 IP
转换为其对应的域名,然后再进行下一步操作。具体的代码如下:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
// 证书验证前置处理
NSString *domain = challenge.protectionSpace.host; // 获取当前请求的 host(域名或者 IP),假设此时为:123.206.23.22
NSString *testHostIP = self.tempDNS[self.testHost];
// 此时服务端返回的证书里的 CN 字段(即证书颁发的域名)与上述 host 可能不一致,
// 因为上述 host 在发请求前已经被我们替换为 IP,所以校验证书时会发现域名不一致而无法通过,导致请求被取消掉,
// 所以,这里在校验证书前做一下替换处理。
if ([domain isEqualToString:testHostIP]) {
domain = self.testHost; // 替换为对应域名:kangzubin.com
}
// 以下逻辑与 AFNetworking -> AFURLSessionManager.m 里的代码一致
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:domain]) {
// 上述 `evaluateServerTrust:forDomain:` 方法用于验证 SSL 握手过程中服务端返回的证书是否可信任,
// 以及请求的 URL 中的域名与证书里声明的的 CN 字段是否一致。
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
其中,evaluateServerTrust:forDomain:
方法的定义如下,可以参考 AFNetworking
中 AFSecurityPolicy
模块的代码。
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
// 创建证书校验策略
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
// 需要验证请求的域名与证书中声明的 CN 字段是否一致
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 绑定校验策略到服务端返回的证书(serverTrust)上
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
// 评估当前 serverTrust 是否可信任,
// 根据苹果文档:https://developer.apple.com/library/ios/technotes/tn2232/_index.html
// 当 result 为 kSecTrustResultUnspecified 或 kSecTrustResultProceed 的情况下,serverTrust 可以被验证通过。
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
上述解决方法只适用于一台服务器的 IP 只配置了一个默认的域名和 SSL 证书的情况。
详细的 Demo 参见:https://github.com/kangzubin/DevDemo/tree/master/TestHTTPDNS
SNI 场景
通常情况下,一台服务器往往会配置多个域名来建立不同 Web 站点或提供不同的服务。例如,域名 a.com
和 b.com
都同时解析到同一 IP 1.1.1.1
上,然后服务器根据客户端请求中的 Host
字段来区分,将请求分配给不同的后台服务来处理。
如前面所述,对于 HTTPS
请求前,需要额外进行 SSL/TLS
握手,但是由于服务器配置了多个域名的 SSL 证书,在握手发送证书时,不知道客户端访问的是哪个域名(因为握手是在某一具体请求之前进行的),所以无法根据不同域名发送不同的证书。
SNI(Server Name Indication)
就是为了解决一个服务器使用多个域名和证书的 SSL/TLS
扩展。它的工作原理是:在进行 SSL/TLS
握手之前,先发送要访问站点的域名(Hostname),这样服务器会根据这个域名返回一个合适的证书。目前,大多数操作系统和浏览器以及主流 HTTP 服务器软件都已经很好地支持 SNI
扩展。
但是同样的问题又来了,当我们采用 HTTPDNS
解析域名,如前所述,请求 URL
中的 host
会被替换成解析后的 IP
,此时握手前发送的 SNI
字段是 IP
,导致服务器最终获取到的"域名"仍然为 IP
,无法找到匹配的证书,只能返回默认的证书或者不返回,所以也会出现 SSL/TLS
握手不成功的错误。
而对于这种场景,iOS 上层网络 API NSURLConnection/NSURLSession
都没有提供相关方法进行 SNI
字段的配置,因此需要 Socket
层级的底层网络库,例如 CFNetwork
,来实现 IP 直连网络请求适配方案。详细的解决方案可以参考这篇文章:《HTTPS SNI 业务场景“IP 直连”方案说明》
https://help.aliyun.com/knowledge_detail/60147.html。
注:以上关于 SNI 的部分文字参考了阿里云 HTTPDNS iOS SDK 的相关技术文档,在此特别感谢!
总结
本文简要介绍了 App 接入 HTTPDNS
服务后,对于两种不同的服务器配置场景 单 IP 单域名证书
和 单 IP 多域名证书(SNI)
,如何解决 HTTPS
请求在 SSL/TLS
握手过程的证书校验问题,不足之处,请多多指正。
参考文档
https://help.aliyun.com/document_detail/30143.html
AFNetWorking Source Code https://github.com/AFNetworking/AFNetworking
觉得好,就打个赏吧
再关注一下喽
以上是关于解决「 HTTPDNS + HTTPS 」的证书校验问题的主要内容,如果未能解决你的问题,请参考以下文章