在 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)
}
注意:kCFNetworkProxiesHTTPEnable 和 kCFNetworkProxiesHTTPSEnable 必须设为 true,否则代理设置不会生效。HTTP 和 HTTPS 使用相同的代理地址和端口 8080,但需要分别启用。
身份验证与地理定位
URLSession 对 kCFProxyUsername 和 kCFProxyPasswordKey 的支持不稳定,在 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 代理 |
|---|---|---|
| 端口 | 8080 | 1080 |
| 工作层级 | 应用层(HTTP) | 传输层 |
| 认证方式 | Proxy-Authorization 头 / Delegate | 握手阶段 / Delegate |
| 协议支持 | HTTP/HTTPS | 任意 TCP |
| ATS 兼容性 | 需配置例外 | 需配置例外 |
| 适用场景 | 网页抓取、API 请求 | 全协议代理、隧道 |
住宅代理实战:并发价格抓取
以下示例使用 async/await 和 TaskGroup 并发抓取多个商品价格,通过住宅代理绕过数据中心 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






