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

Swift中NWConnection的Cookie管理及带认证HTTP代理请求实现

问题描述
  • 未找到使用Swift原生高层API(如URLSession)发起需认证的HTTP代理GET/POST请求的可行方案,相关提问暂未得到解决。
  • 当前仅能通过NWConnection实现需求,但实现方式繁琐。
  • URLSession可自动管理Cookie(自动将前序请求返回的Set-Cookie带入后续请求),尝试在NWConnection中复现该行为失败,询问是否必须手动解析响应头字符串提取Cookie再加入请求,有无更简便方法。
当前NWConnection实现代码
func updateOrderStatusProxy(proxy: String, completion: @escaping (Bool) -> Void) {
    let orderLink = "https://us.supreme.com/checkouts/c/79393a1d2d6098cb2477a49dfd7699bd/thank_you"

    guard let url = URL(string: orderLink) else {
        completion(true)
        return
    }
    
    let proxyDetails = proxy.split(separator: ":").map(String.init)
    guard proxyDetails.count == 4, let port = UInt16(proxyDetails[1]) else {
        completion(false)
        print("Invalid proxy format")
        return
    }

    let proxyEndpoint = NWEndpoint.hostPort(host: .init(proxyDetails[0]),
                                            port: NWEndpoint.Port(integerLiteral: port))
    let proxyConfig = ProxyConfiguration(httpCONNECTProxy: proxyEndpoint, tlsOptions: nil)
    proxyConfig.applyCredential(username: proxyDetails[2], password: proxyDetails[3])

    let parameters = NWParameters.tls
    let privacyContext = NWParameters.PrivacyContext(description: "ProxyConfig")
    privacyContext.proxyConfigurations = [proxyConfig]
    parameters.setPrivacyContext(privacyContext)

    let host = url.host ?? ""
    let path = url.path.isEmpty ? "/" : url.path
    let query = url.query ?? ""
    let fullPath = query.isEmpty ? path : "\(path)?\(query)"

    let connection = NWConnection(
        to: .hostPort(
            host: .init(host),
            port: .init(integerLiteral: UInt16(url.port ?? 443))
        ),
        using: parameters
    )

    connection.stateUpdateHandler = { state in
        switch state {
        case .ready:

            let httpRequest = "GET \(fullPath) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15\r\nAccept-Language: en-US,en;q=0.9\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-Site: none\r\nPriority: u=0, i\r\n\r\n"
            
            connection.send(content: httpRequest.data(using: .utf8), completion: .contentProcessed({ error in
                if let error = error {
                    print("Failed to send request: \(error)")
                    completion(false)
                    return
                }
                
                readAllData(connection: connection) { finalData, readError in
                    if let readError = readError {
                        print("Failed to receive response: \(readError)")
                        completion(false)
                        return
                    }

                    guard let data = finalData else {
                        print("No data received or unable to read data.")
                        completion(false)
                        return
                    }

                    if let body = String(data: data, encoding: .utf8) {
                        completion(true)

                    } else {
                        print("Unable to decode response body.")
                        completion(false)
                    }
                }
            }))

        case .failed(let error):
            print("Connection failed for proxy \(proxyDetails[0]): \(error)")
            completion(false)

        default:
            break
        }
    }

    connection.start(queue: .global())
}
func readAllData(connection: NWConnection,
                         accumulatedData: Data = Data(),
                         completion: @escaping (Data?, Error?) -> Void) {
    
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
        
        if let error = error {
            completion(nil, error)
            return
        }
        
        let newAccumulatedData = accumulatedData + (data ?? Data())

        if isComplete {
            completion(newAccumulatedData, nil)
        } else {
            readAllData(connection: connection,
                             accumulatedData: newAccumulatedData,
                             completion: completion)
        }
    }
}
解决方案

一、用URLSession实现带认证的HTTP代理请求

URLSession原生支持配置带认证的HTTP代理,无需手动拼接HTTP报文,还能自动管理Cookie:

func requestWithAuthenticatedProxy(proxyHost: String, proxyPort: Int, proxyUser: String, proxyPass: String, url: URL, completion: @escaping (Data?, Error?) -> Void) {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: proxyHost,
        kCFNetworkProxiesHTTPPort: proxyPort,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: proxyHost,
        kCFNetworkProxiesHTTPSPort: proxyPort
    ]
    
    let session = URLSession(configuration: config, delegate: ProxyAuthDelegate(user: proxyUser, pass: proxyPass), delegateQueue: .main)
    let task = session.dataTask(with: url) { data, response, error in
        completion(data, error)
        session.finishTasksAndInvalidate()
    }
    task.resume()
}

class ProxyAuthDelegate: NSObject, URLSessionDelegate {
    private let user: String
    private let pass: String
    
    init(user: String, pass: String) {
        self.user = user
        self.pass = pass
        super.init()
    }
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPProxy else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        let credential = URLCredential(user: user, password: pass, persistence: .forSession)
        completionHandler(.useCredential, credential)
    }
}

二、NWConnection中简化Cookie管理的方法

如果必须使用NWConnection,可借助系统API简化Cookie的解析与携带流程:

1. 自动解析响应头中的Cookie

利用HTTPCookie类的原生方法解析Set-Cookie字段,无需手动拆分字符串:

func parseCookies(from responseData: Data, url: URL) -> [HTTPCookie] {
    guard let responseStr = String(data: responseData, encoding: .utf8),
          let headerEndIndex = responseStr.range(of: "\r\n\r\n")?.upperBound else {
        return []
    }
    let headerStr = String(responseStr[..<headerEndIndex])
    let headerFields = headerStr.split(separator: "\r\n").reduce(into: [String: String]()) { dict, line in
        guard let colonIndex = line.range(of: ": ") else { return }
        let key = String(line[..<colonIndex.lowerBound])
        let value = String(line[colonIndex.upperBound...])
        dict[key] = value
    }
    return HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
}

2. 自动携带Cookie到后续请求

将解析得到的Cookie存入系统HTTPCookieStorage,后续请求时直接生成Cookie请求头:

// 存储Cookie
let cookies = parseCookies(from: responseData, url: targetURL)
HTTPCookieStorage.shared.setCookies(cookies, for: targetURL, mainDocumentURL: nil)

// 生成Cookie请求头
func getCookieHeader(for url: URL) -> String? {
    guard let cookies = HTTPCookieStorage.shared.cookies(for: url) else { return nil }
    let cookieFields = HTTPCookie.requestHeaderFields(with: cookies)
    return cookieFields["Cookie"]
}

发送请求时,只需将Cookie头加入HTTP报文即可:

var httpRequest = "GET \(fullPath) HTTP/1.1\r\nHost: \(host)\r\n"
if let cookieHeader = getCookieHeader(for: url) {
    httpRequest += "Cookie: \(cookieHeader)\r\n"
}
httpRequest += "Connection: close\r\nAccept: ...\r\n\r\n"

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 02:39:53