背景:当前iOS客户端请求一直采用Alamofire或者Moya比较多,时常出现系统域名解析出错,运行商不稳定情况,考虑使用三方解决域名解析问题
方案:考虑使用阿里的HTTPDNS域名解析,好了至此走上一条各种坑的道路,下面我们一一说道路的各种崎岖
方案一:使用默认的SLL证书、证书评估使用[SecPolicy]()
- 存在
host:使用SecPolicyCreateSSL(true,host as CFString) - 不存在
host:使用SecPolicyCreateBasicX509 - 结合前后:使用
SecTrustSetPolicies(serverTrust,polices as CFTypeRef)
结果:var result: SecTrustResultType = .invalid if SecTrustEvaluate(serverTrust,&result) == errSecSuccess { return result == .unspecified || result == .proceed } else { reutrn false }
完整代码如下:
class ApiAlamofireSession: Alamofire.Session {
static let shared: ApiAlamofireSession = {
let configuration = URLSessionConfiguration.af.default
configuration.httpAdditionalHeaders = Alamofire.Session.default.session.configuration.httpAdditionalHeaders
let delegate = ApiSessionDelegate()
return ApiAlamofireSession(configuration: configuration, delegate: delegate, serverTrustManager: nil)
}()
}
class ApiSessionDelegate: Alamofire.SessionDelegate {
override func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = task.currentRequest?.value(forHTTPHeaderField: "Host")
if evaluate(serverTrust: challenge.protectionSpace.serverTrust, host: host) {
disposition = .useCredential
credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
trustManager = true
} else {
disposition = .performDefaultHandling
trustManager = false
}
} else {
disposition = .performDefaultHandling
trustManager = false
}
completionHandler(disposition, credential)
}
func evaluate(serverTrust: SecTrust?, host: String?) -> Bool {
guard let serverTrust = serverTrust else {
return false
}
var policies = [SecPolicy]()
if let host = host {
policies.append(SecPolicyCreateSSL(true, host as CFString))
} else {
policies.append(SecPolicyCreateBasicX509())
}
SecTrustSetPolicies(serverTrust, policies as CFTypeRef)
var result: SecTrustResultType = .invalid
if SecTrustEvaluate(serverTrust, &result) == errSecSuccess {
return result == .unspecified || result == .proceed
} else {
return false
}
}
}
DNS解析域名的方法如下:
let host = target.baseURL.host
debugLog("HttpDNS~host:\(host)")
var endpoint = MoyaProvider.defaultEndpointMapping(for: target)
let httpdns = HttpDnsService.sharedInstance()
if let result = httpdns?.resolveHostSync(host,by:HttpdnsQueryIPType.both),let url = URL(string:endpoint.url),trustManager == true
{
var urlComponents = URLComponents(url:url, resolvingAgainstBaseURL: false)
if result.hasIpv4Address() {
urlComponents?.host = result.firstIpv4Address()
debugLog("HttpDNS~Get IPv4 \(result.firstIpv4Address())")
let resultUrl = urlComponents?.url ?? url
//设置原始的host头
var headers = endpoint.httpHeaderFields ?? [:]
headers["Host"] = host
debugLog("HttpDNS~Get for host\(String(describing: host)) from HTTPDNS Successfully! resultUrl:\(resultUrl)")
endpoint = getIpTypeEndpoint(resultUrl: resultUrl.absoluteString, target: target, headers: headers)
debugLog("HttpDNS~endpont:\(endpoint.url)")
}else if result.hasIpv6Address() {
urlComponents?.host = result.firstIpv6Address()
debugLog("HttpDNS~Get IPv6 \(result.firstIpv6Address())")
let resultUrl = urlComponents?.url ?? url
//设置原始的host头
var headers = endpoint.httpHeaderFields ?? [:]
headers["Host"] = host
debugLog("HttpDNS~Get for host\(String(describing: host)) from HTTPDNS Successfully! resultUrl:\(resultUrl)")
endpoint = getIpTypeEndpoint(resultUrl: resultUrl.absoluteString, target: target, headers: headers)
debugLog("HttpDNS~endpont:\(endpoint.url)")
}else{
endpoint = getIpTypeEndpoint(resultUrl: target.baseURL.absoluteString + target.path, target: target, headers: target.headers)
}
}else{
endpoint = getIpTypeEndpoint(resultUrl: target.baseURL.absoluteString + target.path, target: target, headers: target.headers)
}
switch target {
case .pwd_login:
timeoutInterval = 15.0
default:
timeoutInterval = 15.0
}
return endpoint
}
方案一的缺点:报错如下
.serverCertificateHasBadDate, // -1201: 证书日期错误 (过期或未生效)
.serverCertificateUntrusted, // -1202: 证书不被信任 (自签名、链不完整)
.serverCertificateHasUnknownRoot, // -1203: 证书根不受信任
.serverCertificateNotYetValid, // -1204: 证书尚未生效
.clientCertificateRejected, // -1205: 客户端证书被拒绝
.clientCertificateRequired: // -1206: 服务器要求客户端证书
-
用
SecPolicyCreateBasicX509 -
系统期望的是 SSL policy
-
校验策略不匹配 → TLS handshake 直接失败
-
就直接报上面的错误了,自己验证通过 ≠ 系统 TLS 认可
方案二:使用动态ServerTrustManager可以动态地将IP地址映射到其对应的域名验证策略
/// 它的作用是在进行证书验证时,强制使用我们指定的 `validationHost` (原始域名),
/// 而忽略 Alamofire 传入的 `host` (IP 地址)。
**fileprivate** **class** DomainOverridingTrustEvaluator: ServerTrustEvaluating {
**private** **let** validationHost: String
**private** **let** underlyingEvaluator: ServerTrustEvaluating
**init**(validationHost: String, underlyingEvaluator: ServerTrustEvaluating) {
**self**.validationHost = validationHost
**self**.underlyingEvaluator = underlyingEvaluator
}
**func** evaluate(_ trust: SecTrust, forHost host: String) **throws** {
// 【核心】: 忽略传入的 `host` (IP 地址),强制使用 `validationHost` (域名)
**try** underlyingEvaluator.evaluate(trust, forHost: **self**.validationHost)
}
}
/// 【新增】: 这是一个支持 HTTPDNS 的动态 ServerTrustManager。
/// 它可以动态地将 IP 地址映射到其对应的域名验证策略。
**class** HttpDnsServerTrustManager: ServerTrustManager {
**private** **var** ipToDomainMap: [String: String] = [:]
**private** **let** lock = NSLock()
/// 外部应在发起请求前,调用此方法来注册 IP 和域名的映射关系。
**func** add(ip: String, forDomain domain: String) {
lock.lock(); **defer** { lock.unlock() }
ipToDomainMap[ip] = domain
}
/// 重写 Alamofire 的核心方法。
**override** **func** serverTrustEvaluator(forHost host: String) **throws** -> ServerTrustEvaluating? {
lock.lock(); **defer** { lock.unlock() }
// `host` 在我们的场景中是 IP 地址。
// 1. 尝试从动态映射中找到这个 IP 对应的原始域名。
**if** **let** domain = ipToDomainMap[host] {
// 2. 如果找到了域名,就从父类的 evaluators 字典中查找为【该域名】配置的评估器。
**if** **let** domainEvaluator = evaluators[domain] {
// 3. 返回一个包装后的评估器,确保验证时使用域名而不是 IP。
**return** DomainOverridingTrustEvaluator(validationHost: domain, underlyingEvaluator: domainEvaluator)
}
}
// 4. 如果找不到映射,则回退到默认行为。
**return** **try** **super**.serverTrustEvaluator(forHost: host)
}
}
使用动态域名解析可以解决大部分问题,但是还是有部分用户报错
意外不意外,惊喜不惊喜😒
- 排查结果一:
SSL/TLS 证书或安全协议错误::errorCode: 6, errorUserInfo: ["NSLocalizedDescription": "Server trust evaluation failed due to reason: SecTrust evaluation failed with error: “*.123.com”证书不符合标准", "NSUnderlyingError": Alamofire.AFError.serverTrustEvaluationFailed(reason: Alamofire.AFError.ServerTrustFailureReason.trustEvaluationFailed(error: Optional(Error Domain=NSOSStatusErrorDomain Code=-67825 "“*.123.com”证书不符合标准" UserInfo={NSLocalizedDescription=“*.123.com”证书不符合标准, NSUnderlyingError=0x1097cd800 {Error Domain=NSOSStatusErrorDomain Code=-67825 "证书0 “*.123.com”有错误:本次使用需要证书透明度验证;" UserInfo={NSLocalizedDescription=证书0 “*.123.com”有错误:本次使用需要证书透明度验证;奇怪吧又戳戳来一堆错误 - 好的不算差,最起码有错误日志不是,显示证书没有透明度验证?
- 从 iOS 12.1.1+ 开始,Apple 强制要求所有 TLS 证书必须包含 SCT(Signed Certificate Timestamp) 记录,这是证书透明度的一部分。您的服务器证书可能:
-
- ❌ 没有包含 SCT 日志
-
- ❌ SCT 日志不完整或过期
-
- ❌ 使用了旧的 CA 签发的证书
好吧兄弟们把锅甩出去吧,找运维去哈
十分钟后。。。。。。。。
- ✅ 证书本身没问题
- ✅ 是否含有CT政策: 符合
- ✅ 颁发者:RapidSSL TLS RSA CA G1(DigiCert)
- ✅ 信任状态:可信
- ✅ 有效期:正常(剩余 375 天)
证书本身符合 CT 要求,问题在于其他地方,好吧继续。。。。。
因为我们用的是HTTPDNS将域名替换为IP,会导致urlComponents.host = ip看着也没错呀,HTTPDNS的作用不就是这个吗?????
好的,那就查查这个流程吧,到底干了啥?
- HTTPDNS 将 123.com 解析为 IP(如 1.2.3.4)
- 请求 URL 变成 https://1.2.3.4/api/xxx
- 服务器返回 *.123.com 证书
- iOS 验证证书时,发现 URL host 是 IP,但证书是域名
- CT 验证失败(因为 SNI 或 host 不匹配)
真相大白了,我们的流程就是这样的呀
所以再加个容错吧,万分之一的概率出错
解决方法如下
- HTTPDNS 解析:123.com → 1.2.3.4 ↓
- URL 变成:https://1.2.3.4/api/xxx ↓
- trustManager.add(ip: "1.2.3.4", forDomain: "123.com") ↓
- 发起请求,Host header = 123.com ↓
- ServerTrustManager 验证证书:
- host = "1.2.3.4" (URL 的 host)
- 查找 ipToDomainMap["1.2.3.4"] → "123.com"
- 查找 evaluators["123.com"] → DefaultTrustEvaluator()
- 使用域名 "123.com" 验证证书 ✅
至此全部解决兄弟们,加一次域名请求的兜底,保证失效状态的重试