如何区分URLSession中证书验证失败与其他取消导致的URLError?
区分证书验证失败与通用取消错误的解决方案
在本地HTTPS通信做证书固定时,默认返回的取消错误(-999)无法区分是证书验证失败还是其他操作导致的取消,又不想让UI代码侵入网络层,可以试试这几个方案:
方案1:封装URLSession+线程安全字典记录失败状态
创建一个封装URLSession的网络类,内部实现代理逻辑,同时用线程安全字典记录每个task是否因证书验证失败被取消。当捕获到通用取消错误时,查询字典判断是否为证书问题,再抛出自定义错误。
代码示例:
import Foundation // 自定义证书验证失败错误 enum NetworkSecurityError: Error { case certificateValidationFailed } class SecureNetworkSession { private let session: URLSession private var failedCertTasks: [URLSessionTask: Bool] = [:] private let accessQueue = DispatchQueue(label: "com.secure.network.session.queue") init() { let config = URLSessionConfiguration.default self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) } func fetchData(from url: URL) async throws -> (Data, URLResponse) { let task = session.dataTask(with: url) do { let result = try await session.data(from: url) // 请求成功后清除记录 accessQueue.sync { failedCertTasks.removeValue(forKey: task) } return result } catch is URLError where (error as? URLError)?.code == .cancelled { let isCertFailure = accessQueue.sync { failedCertTasks[task] ?? false } if isCertFailure { throw NetworkSecurityError.certificateValidationFailed } else { throw error } } } } extension SecureNetworkSession: URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust else { return (.performDefaultHandling, nil) } var evalError: CFError? let isValid = SecTrustEvaluateWithError(trust, &evalError) guard isValid else { // 标记当前task为证书验证失败 if let task = challenge.task { accessQueue.sync { failedCertTasks[task] = true } } return (.cancelAuthenticationChallenge, nil) } return (.useCredential, URLCredential(trust: trust)) } }
方案2:利用Task Local存储验证结果
借助Swift的Task Local特性,在代理的证书验证失败逻辑中,给当前异步任务标记一个标识。当捕获到取消错误时,读取这个标识判断是否为证书问题。
代码示例:
import Foundation enum NetworkSecurityError: Error { case certificateValidationFailed } // 定义Task Local的键 private let isCertValidationFailure = TaskLocalKey<Bool>() private struct TaskLocalKey<T>: RawRepresentable { typealias RawValue = String let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } class CertValidationDelegate: NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust else { return (.performDefaultHandling, nil) } var evalError: CFError? let isValid = SecTrustEvaluateWithError(trust, &evalError) guard isValid else { // 给当前Task标记证书验证失败 await Task.setValue(true, forKey: isCertValidationFailure) return (.cancelAuthenticationChallenge, nil) } return (.useCredential, URLCredential(trust: trust)) } } // 使用示例 func fetchSecureData(url: URL) async throws -> Data { let delegate = CertValidationDelegate() let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) do { let (data, _) = try await session.data(from: url) return data } catch is URLError where (error as? URLError)?.code == .cancelled { // 读取Task Local的标记 if let failed = await Task.value(forKey: isCertValidationFailure) as? Bool, failed { throw NetworkSecurityError.certificateValidationFailed } else { throw error } } }
方案3:自定义Error的URLSessionTask(进阶)
如果需要更精细的控制,可以自定义URLSessionDataTask子类,在证书验证失败时给task设置自定义错误属性。不过这种方式需要重写URLSession的task创建逻辑,适合复杂的网络层架构。
内容的提问来源于stack exchange,提问作者ph1psG
相关产品推荐
相关产品推荐

