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
相关产品推荐
相关产品推荐

