You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何区分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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:07:38