在 Swift 中使用代理:URLSession 完整指南

面向 iOS 和 macOS 开发者的代码指南,讲解如何通过 URLSessionConfiguration.connectionProxyDictionary 配置 HTTP/HTTPS/SOCKS5 代理,处理身份验证、地理定位、并发请求及生产环境重试策略。

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

在 Swift 中使用代理是 iOS 和 macOS 开发者进行网页抓取、API 区域测试和访问受地理限制内容时的核心技能。URLSession 提供了原生代理支持,但配置方式并不直观——connectionProxyDictionary 使用 CFNetwork 常量键,身份验证需要手动处理,SOCKS5 又涉及另一组键。本文以代码为先,逐步讲解如何在 Swift 中正确配置 URLSession 代理,涵盖 HTTP/HTTPS 代理、身份验证、地理定位、SOCKS5、并发请求及生产环境最佳实践。

为什么在 Swift 中使用代理

许多 API 端点和网站会拦截来自数据中心 IP 的请求。如果你从 AWS、GCP 或 Azure 的 IP 段发起请求,目标服务器可能直接返回 403 或触发验证码挑战。住宅代理使用来自真实 ISP 的 IP 地址,请求看起来像普通用户发出的,成功率显著更高。

此外,区域锁定内容(如流媒体、本地化搜索结果)要求请求来自特定地理位置。通过在代理用户名中编码地理参数,你可以在不改应用代码的情况下切换出口地区。

Apple 的 URLSession 框架底层使用 CFNetwork,代理配置通过 URLSessionConfiguration.connectionProxyDictionary 实现。这个字典使用 kCFNetworkProxies* 系列常量作为键。更多细节可参考 Apple 官方 URLSessionConfiguration 文档

配置 HTTP/HTTPS 代理

以下代码展示如何创建一个通过 gate.proxyhat.com:8080 路由的 URLSession

import Foundation

func makeProxiedSession() -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort: 8080,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort: 8080
    ]
    return URLSession(configuration: config)
}

注意:kCFNetworkProxiesHTTPEnablekCFNetworkProxiesHTTPSEnable 必须设为 true,否则代理设置不会生效。HTTP 和 HTTPS 使用相同的代理地址和端口 8080,但需要分别启用。

身份验证与地理定位

URLSessionkCFProxyUsernamekCFProxyPasswordKey 的支持不稳定,在 iOS 和 macOS 上行为不一致。推荐两种方式处理代理身份验证。

方式一:手动设置 Proxy-Authorization 头

将用户名和密码编码为 Base64,通过 Proxy-Authorization: Basic 头传递。地理定位和会话参数编码在用户名中:

import Foundation

func proxyAuthHeader(username: String, password: String) -> String {
    let credentials = "\(username):\(password)"
    let data = credentials.data(using: .utf8)!
    return "Basic \(data.base64EncodedString())"
}

func fetchWithProxyAuth(url: URL) async throws -> Data {
    let session = makeProxiedSession()
    var request = URLRequest(url: url)
    let token = proxyAuthHeader(
        username: "user-country-US-city-newyork-session-abc123",
        password: "pass"
    )
    request.setValue(token, forHTTPHeaderField: "Proxy-Authorization")
    let (data, response) = try await session.data(for: request)
    guard let http = response as? HTTPURLResponse,
          http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return data
}

用户名格式为 user-country-US-city-newyork-session-abc123,其中 country-US 指定美国出口,city-newyork 指定纽约,session-abc123 保持粘性会话(同一出口 IP)。

方式二:实现 URLSessionDelegate 处理 407 挑战

当代理返回 407 Proxy Authentication Required 时,URLSession 会触发认证挑战回调。实现委托方法来响应:

import Foundation

final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
    let username: String
    let password: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        let space = challenge.protectionSpace
        if space.authenticationMethod == NSURLAuthenticationMethodHTTPProxy
            || space.authenticationMethod == NSURLAuthenticationMethodHTTPSProxy {
            let credential = URLCredential(
                user: username,
                password: password,
                persistence: .forSession
            )
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

SOCKS5 代理配置

SOCKS5 代理使用不同的 CFNetwork 键,端口为 1080。SOCKS5 工作在传输层,不修改 HTTP 头,适合需要协议无关代理的场景:

import Foundation

func makeSOCKS5Session() -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesSOCKSEnable: true,
        kCFNetworkProxiesSOCKSProxy: "gate.proxyhat.com",
        kCFNetworkProxiesSOCKSPort: 1080
    ]
    return URLSession(configuration: config)
}

SOCKS5 代理同样需要身份验证。由于 SOCKS5 的认证在握手阶段完成,Proxy-Authorization 头不适用。使用 URLSessionDelegate 方式处理 SOCKS5 认证更可靠。参考 Apple URLSessionDelegate 文档 了解更多委托方法。

代理类型对比

特性HTTP/HTTPS 代理SOCKS5 代理
端口80801080
工作层级应用层(HTTP)传输层
认证方式Proxy-Authorization 头 / Delegate握手阶段 / Delegate
协议支持HTTP/HTTPS任意 TCP
ATS 兼容性需配置例外需配置例外
适用场景网页抓取、API 请求全协议代理、隧道

住宅代理实战:并发价格抓取

以下示例使用 async/awaitTaskGroup 并发抓取多个商品价格,通过住宅代理绕过数据中心 IP 封锁,并使用 Codable 解码 JSON 响应:

import Foundation

struct PriceData: Codable {
    let product: String
    let price: Double
    let currency: String
}

func scrapePrices(urls: [URL]) async throws -> [PriceData] {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort: 8080,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort: 8080
    ]
    config.httpMaximumConnectionsPerHost = 50

    let delegate = ProxyAuthDelegate(
        username: "user-country-US-city-newyork-session-abc123",
        password: "pass"
    )
    let session = URLSession(
        configuration: config,
        delegate: delegate,
        delegateQueue: nil
    )

    return try await withThrowingTaskGroup(of: PriceData?.self) { group in
        for url in urls {
            group.addTask {
                var request = URLRequest(url: url)
                request.setValue(
                    proxyAuthHeader(
                        username: "user-country-US-city-newyork-session-abc123",
                        password: "pass"
                    ),
                    forHTTPHeaderField: "Proxy-Authorization"
                )
                do {
                    let (data, response) = try await session.data(for: request)
                    guard let http = response as? HTTPURLResponse,
                          http.statusCode == 200 else { return nil }
                    return try JSONDecoder().decode(PriceData.self, from: data)
                } catch {
                    return nil
                }
            }
        }
        var results: [PriceData] = []
        for try await result in group {
            if let r = result { results.append(r) }
        }
        return results
    }
}

httpMaximumConnectionsPerHost 设为 50 允许高并发。对于 100 个 URL 的批量抓取,TaskGroup 可高效并行处理。ProxyHat 网关支持 99.9% 正常运行时间,住宅代理平均响应延迟约 200ms。

生产环境最佳实践

TLS 处理

如果目标服务器使用自签名证书或需要自定义 TLS 验证,实现委托方法处理证书挑战:

final class TLSDelicateDelegate: NSObject, URLSessionDelegate {
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        if challenge.protectionSpace.authenticationMethod
            == NSURLAuthenticationMethodServerTrust {
            let trust = challenge.protectionSpace.serverTrust
            completionHandler(.useCredential, URLCredential(trust: trust))
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

安全警告:上述代码跳过了证书验证,仅适用于开发调试。生产环境应使用默认证书验证或实现严格的证书固定(pinning)策略。

指数退避重试

网络请求可能因临时故障失败。实现指数退避重试策略提高可靠性:

import Foundation

func fetchWithRetry(
    url: URL,
    maxRetries: Int = 3,
    baseDelay: UInt64 = 1_000_000_000
) async throws -> Data {
    var delay = baseDelay
    for attempt in 0..<maxRetries {
        do {
            return try await fetchWithProxyAuth(url: url)
        } catch {
            if attempt == maxRetries - 1 { throw error }
            try await Task.sleep(nanoseconds: delay)
            delay *= 2
        }
    }
    throw URLError(.timedOut)
}

初始延迟 1 秒,每次失败后翻倍(1s → 2s → 4s),最多重试 3 次。

App Transport Security (ATS)

Apple 的 ATS 默认要求所有连接使用 TLS 1.2+。通过代理的 HTTPS 连接通常不受影响,但如果代理使用 HTTP 隧道(CONNECT 方法),你可能需要在 Info.plist 中配置 ATS 例外。详见 Apple ATS 文档

关键 ATS 配置项:

  • NSAllowsArbitraryLoads:允许所有 HTTP 连接(不推荐用于生产)
  • NSExceptionDomains:为特定域名配置例外

设备端隐私

在 iOS 上使用代理时,注意:

  • 代理流量不经过 iOS 的 Network Extension 框架,VPN 配置不受影响
  • URLSession 的代理设置仅影响该 session,不全局生效
  • 在 App Store 审核中,代理用途需符合 App Store 审核指南

curl 对比验证

在 Swift 代码之前,先用 curl 验证代理连通性:

# HTTP 代理 + 地理定位
curl -x http://user-country-US-city-newyork-session-abc123:pass@gate.proxyhat.com:8080 \
  https://httpbin.org/ip

# SOCKS5 代理
curl -x socks5://user-country-US-city-newyork-session-abc123:pass@gate.proxyhat.com:1080 \
  https://httpbin.org/ip

ProxyHat 同时提供 Python 和 Node.js SDK,使用相同的 gate.proxyhat.com 网关。如果你的后端服务也使用代理,可参考 ProxyHat 文档 获取 SDK 集成指南。

伦理与法律考量

使用代理抓取数据时,务必遵守以下原则:

  • 优先使用官方 API:如果目标提供 API,优先使用而非抓取网页
  • 遵守 robots.txt:尊重网站的爬取规则
  • CFAA(美国):美国《计算机欺诈和滥用法案》可能将未经授权的访问定为犯罪,仅抓取公开数据
  • GDPR(欧盟):涉及欧盟用户个人数据时需遵守 GDPR,详见 GDPR 官方指南
  • App Store 政策:iOS 应用中使用代理需符合 Apple 审核指南,不得用于绕过内购或地理限制内容

更多代理使用场景,可参考我们的 网页抓取用例SERP 追踪用例。查看 定价方案可用代理位置

关键要点

  • URLSessionConfiguration.connectionProxyDictionary 使用 kCFNetworkProxies* 常量键配置代理
  • 代理身份验证推荐使用 Proxy-Authorization 头或 URLSessionDelegate,而非 kCFProxyUsername/PasswordKey
  • 地理定位和会话参数编码在用户名中:user-country-US-city-newyork-session-abc123
  • SOCKS5 使用 kCFNetworkProxiesSOCKS* 键,端口 1080
  • 住宅代理可绕过数据中心 IP 封锁,适合网页抓取和区域测试
  • 生产环境需实现重试、TLS 处理和 ATS 配置
  • 始终遵守法律和平台政策,优先使用官方 API

准备开始了吗?

通过AI过滤访问148多个国家的5000多万个住宅IP。

查看价格住宅代理
← 返回博客