在 Kotlin 中使用代理(Kotlin proxy)是 Android 与后端开发者绕过 IP 限制、做 Kotlin web scraping 以及做地理定位请求时绕不开的话题。无论你用 Ktor Client 还是 OkHttp,代理认证、IP 轮换、TLS 与并发控制都有不少坑。本文以代码优先的方式,带你从项目搭建一路走到生产级加固。
为什么在 Kotlin 中使用代理如此重要
当你直接用 HttpClient 请求目标站点时,出口 IP 是你服务器或设备的真实 IP。许多平台——尤其是社交媒体、电商和票务站点——会根据 ASN 封禁来自数据中心(datacenter)IP 段的请求。根据 MDN 关于 HTTP 429 的说明,服务端可以通过 Retry-After 头强制客户端退避;而更严格的反爬系统会直接返回 403 或触发 CAPTCHA。
住宅代理(residential proxy)使用来自真实 ISP 的 IP 地址,目标站点很难将其与普通用户区分开来。这就是 Ktor client proxy 和 OkHttp proxy authentication 方案的核心价值:让你的请求看起来像来自真实用户。
项目搭建:Ktor 3 与 OkHttp 基线
首先在 build.gradle.kts 中添加依赖。Ktor 3.x 支持 CIO 和 OkHttp 引擎,OkHttp 用于 Android 与 JVM 后端都适用。
// build.gradle.kts
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
implementation("io.ktor:ktor-client-okhttp:3.0.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
下面是 Ktor Client 使用 CIO 引擎的最小示例,先不设代理,作为基线:
// KtorBaseline.kt
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun fetchBaseline(url: String): String {
val client = HttpClient(CIO) {
engine {
// CIO 引擎默认不原生支持 HTTP 代理,需要用 OkHttp 引擎或手动配置
}
}
return try {
client.get(url).bodyAsText()
} finally {
client.close()
}
}
OkHttp 的基线则更直接——通过 java.net.Proxy 指定代理地址:
// OkHttpBaseline.kt
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
fun fetchWithProxy(url: String): String {
val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
val client = OkHttpClient.Builder()
.proxy(proxy)
.build()
val req = Request.Builder().url(url).build()
client.newCall(req).execute().use { resp ->
return resp.body?.string() ?: ""
}
}
但上面的 OkHttp 基线缺少代理认证。如果代理需要用户名密码,必须配置 Authenticator 来处理 407 挑战。我们稍后在生产加固部分详细讲解。
通过 ProxyHat 网关路由请求
ProxyHat 的网关地址是 gate.proxyhat.com,HTTP 端口 8080,SOCKS5 端口 1080。地理定位和会话粘性通过用户名编码实现,例如 user-country-DE-city-berlin 表示德国柏林出口,-session-abc123 表示粘性会话。
在 Ktor 中,代理认证是引擎特定的。OkHttp 引擎支持 proxy 和 proxyAuthenticator,而 CIO 引擎不原生支持 HTTP 代理。因此推荐使用 OkHttp 引擎来获得完整的代理支持。你可以在 defaultRequest 中统一注入 Proxy-Authorization: Basic 头:
// KtorProxyHat.kt
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy
fun createProxyHatClient(
username: String,
password: String,
country: String = "US",
city: String? = null,
session: String? = null
): HttpClient {
// 构建带地理定位和会话的用户名
val fullUser = buildString {
append(username)
append("-country-$country")
if (city != null) append("-city-$city")
if (session != null) append("-session-$session")
}
val proxyAuthenticator = Authenticator { _: Route?, response ->
val credential = Credentials.basic(fullUser, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
val okHttpBuilder = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator(proxyAuthenticator)
return HttpClient(OkHttp) {
engine { preconfigured = okHttpBuilder.build() }
defaultRequest {
// 也可以在此统一加 User-Agent 等头
header("User-Agent", "MyKotlinBot/1.0")
}
}
}
suspend fun main() {
val client = createProxyHatClient(
username = "user",
password = "pass",
country = "DE",
city = "berlin",
session = "abc123"
)
client.use {
val resp: HttpResponse = it.get("https://httpbin.org/ip")
println(resp.bodyAsText())
}
}
上面的代码展示了 OkHttp proxy authentication 的标准做法:通过 Authenticator 在收到 407 响应时注入 Proxy-Authorization 头。Ktor 的 defaultRequest 用于统一添加业务请求头。
SOCKS5 配置:端口 1080
SOCKS5 代理在某些场景下比 HTTP 代理更灵活,因为它可以代理任意 TCP 流量。在 JVM 中,你可以通过系统属性配置 SOCKS5 认证:
// Socks5Proxy.kt
import java.net.InetSocketAddress
import java.net.Proxy
import okhttp3.OkHttpClient
import okhttp3.Request
fun fetchViaSocks5(url: String): String {
// 设置 SOCKS5 认证的系统属性
System.setProperty("java.net.socks.username", "user-country-US-session-xyz789")
System.setProperty("java.net.socks.password", "pass")
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080))
val client = OkHttpClient.Builder()
.proxy(proxy)
.build()
val req = Request.Builder().url(url).build()
client.newCall(req).execute().use { resp ->
return resp.body?.string() ?: ""
}
}
注意:JVM 的 SOCKS5 认证通过系统属性全局生效,不适合多租户场景。如果需要按请求切换 SOCKS5 凭证,请使用
java.net.Authenticator.setDefault(...)自定义认证器。
住宅代理与并发抓取:协程实战
数据中心 IP 容易被社交媒体和应用平台识别并封禁。例如,Instagram、TikTok 等平台维护 ASN 黑名单,来自 AWS、GCP、Azure 等 IP 段的请求往往直接被拒。住宅代理的 IP 来自真实 ISP,更难被检测。
下面是一个完整的协程并发抓取示例,使用 async / awaitAll 做扇出请求,用 Semaphore 控制并发度,避免触发目标站点的速率限制:
// ConcurrentScrape.kt
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.UUID
data class ScrapeResult(val url: String, val status: Int, val body: String, val durationMs: Long)
suspend fun scrapeConcurrently(urls: List<String>): List<ScrapeResult> = coroutineScope {
val semaphore = Semaphore(20) // 限制为 20 并发
val client = createRotatingClient("user", "pass")
client.use { c ->
val deferred = urls.map { url ->
async(Dispatchers.IO) {
semaphore.withPermit {
val start = System.currentTimeMillis()
try {
val resp = c.get(url)
ScrapeResult(
url = url,
status = resp.status.value,
body = resp.bodyAsText(),
durationMs = System.currentTimeMillis() - start
)
} catch (e: Exception) {
ScrapeResult(url, -1, e.message ?: "error", System.currentTimeMillis() - start)
}
}
}
}
deferred.awaitAll()
}
}
fun createRotatingClient(username: String, password: String): HttpClient {
// 每个请求使用不同 session ID 实现轮换
val proxyAuthenticator = Authenticator { _: Route?, response ->
val sessionId = UUID.randomUUID().toString().take(8)
val cred = Credentials.basic("$username-country-US-session-$sessionId", password)
response.request.newBuilder()
.header("Proxy-Authorization", cred)
.build()
}
val okHttp = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator(proxyAuthenticator)
.connectTimeout(java.time.Duration.ofSeconds(10))
.readTimeout(java.time.Duration.ofSeconds(30))
.build()
return HttpClient(OkHttp) {
engine { preconfigured = okHttp }
}
}
fun main() = runBlocking {
val urls = (1..50).map { "https://httpbin.org/delay/1?id=$it" }
val results = scrapeConcurrently(urls)
val success = results.count { it.status == 200 }
println("成功: $success / ${results.size}")
println("平均耗时: ${results.map { it.durationMs }.average().toLong()} ms")
}
上面的代码通过在 Authenticator 中为每次 407 挑战生成新的 session-xxx,实现了每请求 IP 轮换。Semaphore(20) 将并发限制在 20 个请求,避免一次性发出过多请求而被封禁。
生产级加固
OkHttp Authenticator 处理 407 挑战
OkHttp 的 Authenticator 接口会在收到 407 Proxy Authentication Required 时被调用。你应该在此处注入 Proxy-Authorization 头。注意 Authenticator 返回 null 时 OkHttp 会放弃重试:
val proxyAuthenticator = Authenticator { _, response ->
if (response.code == 407) {
val cred = Credentials.basic("user-country-DE-session-abc123", "pass")
response.request.newBuilder()
.header("Proxy-Authorization", cred)
.build()
} else null
}
重试与超时配置
val okHttp = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator(proxyAuthenticator)
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
// 连接池:保持 5 个空闲连接,存活 5 分钟
.connectionPool(okhttp3.ConnectionPool(5, 5, java.util.concurrent.TimeUnit.MINUTES))
.build()
TLS 配置
在抓取 HTTPS 目标时,确保 TLS 配置正确。OkHttp 默认使用系统的 TrustManager。如果你需要自定义证书(例如自签名测试环境),可以配置 sslSocketFactory:
import okhttp3.OkHttpClient
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
// ⚠️ 仅用于测试环境,生产环境请勿跳过证书验证
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())
val unsafeClient = OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.build()
Android NetworkSecurityConfig
在 Android 上,从 Android 9 (API 28) 开始,默认不信任用户安装的 CA 证书。如果你的代理使用 MITM 证书做调试,需要在 res/xml/network_security_config.xml 中声明:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">gate.proxyhat.com</domain>
</domain-config>
</network-security-config>
然后在 AndroidManifest.xml 的 <application> 标签中引用:android:networkSecurityConfig="@xml/network_security_config"。
代理类型对比
| 特性 | 住宅代理 | 数据中心代理 | 移动代理 |
|---|---|---|---|
| IP 来源 | 真实 ISP | 云服务商 | 移动运营商 |
| 被封锁概率 | 低 | 高 | 极低 |
| 延迟 | 中(50-200ms) | 低(10-50ms) | 高(100-500ms) |
| 适用场景 | 社交媒体、电商 | 批量 API 调用 | 移动端应用测试 |
| 价格 | 中高 | 低 | 高 |
对于社交媒体和移动应用目标,住宅代理通常是最佳选择。对于纯 API 调用(无 ASN 封禁),数据中心代理更快更便宜。你可以在 ProxyHat 代理位置 页面查看可用的国家和城市。
伦理与法律考量
抓取数据前,请务必遵守以下原则:
- 仅抓取公开数据:不要绕过登录墙或付费墙。
- 遵守 robots.txt:尊重目标站点的爬虫协议。
- 美国 CFAA:根据 美国司法部相关说明,未授权访问计算机系统可能违反《计算机欺诈和滥用法》(CFAA)。
- 欧盟 GDPR:抓取涉及个人数据的内容受 GDPR 约束,违规罚款可达全球年营收的 4%。
- 优先使用官方 API:如果目标平台提供 API,优先使用 API 而非网页抓取。
更多抓取场景可参考 ProxyHat 网页抓取用例 和 SERP 追踪用例。完整 API 文档请查阅 ProxyHat 官方文档。
关键要点
- OkHttp 引擎优先:Ktor 的 OkHttp 引擎提供完整的代理支持,包括 407 认证和连接池。
- 用户名编码地理定位:
user-country-DE-city-berlin-session-abc123格式同时控制出口位置和会话粘性。 - Semaphore 控制并发:在协程中使用
Semaphore限制并发请求数,避免触发封禁。 - 每请求轮换:在
Authenticator中生成新的 session ID 实现每请求 IP 轮换。 - 住宅代理用于社交目标:数据中心 IP 在社交媒体和移动应用上被封禁概率高,住宅 IP 更可靠。
- 伦理优先:仅抓取公开数据,遵守 CFAA 和 GDPR,优先使用官方 API。
准备好开始了吗?查看 ProxyHat 定价方案,选择适合你的住宅、移动或数据中心代理套餐。






