在 Kotlin 中使用代理:Ktor Client 与 OkHttp 实战指南

面向 Android 与 Kotlin 后端开发者的代码优先指南,涵盖 Ktor Client 与 OkHttp 的代理配置、住宅 IP 轮换、SOCKS5、并发抓取与生产级加固。

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp
本文目录

在 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 proxyOkHttp 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 引擎支持 proxyproxyAuthenticator,而 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 定价方案,选择适合你的住宅、移动或数据中心代理套餐。

常见问题

在 Kotlin 中使用代理是什么?

在 Kotlin 中使用代理是指通过 Ktor Client 或 OkHttp 等 HTTP 客户端库,将请求路由到代理服务器(如 gate.proxyhat.com:8080),由代理服务器转发到目标站点。代理可以隐藏真实 IP、实现地理定位、轮换出口 IP 以避免封禁。在 Kotlin 中,代理认证通常通过 OkHttp 的 Authenticator 接口处理 407 挑战,或在用户名中编码地理定位和会话信息。

为什么在 Kotlin 中使用代理对代理用户很重要?

代理用户需要代理来绕过 IP 封禁、实现地理定位请求、以及进行大规模网页抓取。许多社交媒体和电商平台会封禁数据中心 IP 段的请求,住宅代理使用真实 ISP 的 IP 地址可以显著降低被封禁的概率。在 Kotlin 中正确配置代理可以确保请求成功率和数据采集的稳定性,尤其对于需要 100 并发会话以上的场景至关重要。

哪种代理类型最适合在 Kotlin 中使用?

选择代理类型取决于使用场景。住宅代理适合社交媒体、电商和移动应用目标,因为其 IP 来自真实 ISP,被封锁概率低。数据中心代理适合纯 API 调用等无 ASN 封禁的场景,延迟更低(10-50ms)。移动代理适合移动端应用测试,IP 来自移动运营商,被封锁概率极低但延迟较高(100-500ms)。对于 Kotlin web scraping,住宅代理通常是最佳选择。

如何在 Kotlin 中实现代理时避免被封禁?

避免封禁的关键策略包括:使用住宅代理而非数据中心代理、通过 Semaphore 控制并发度(建议 20 并发以内)、在 OkHttp Authenticator 中为每个请求生成新的 session ID 实现 IP 轮换、设置合理的超时和重试策略、添加真实的 User-Agent 头、以及遵守目标站点的 robots.txt。此外,优先使用官方 API 而非网页抓取,并确保仅抓取公开数据以遵守 CFAA 和 GDPR。

Ktor Client 和 OkHttp 在代理配置上有什么区别?

Ktor 的 CIO 引擎不原生支持 HTTP 代理,需要使用 OkHttp 引擎来获得完整的代理支持。OkHttp 引擎通过 preconfigured 参数接受一个预配置的 OkHttpClient,支持 proxy 和 proxyAuthenticator。代理认证是引擎特定的,Ktor 的 defaultRequest 可以统一添加业务请求头但不能直接处理代理认证。在 OkHttp 中,407 挑战由 Authenticator 接口处理,返回带有 Proxy-Authorization 头的请求即可。

准备好开始了吗?

覆盖 148+ 国家的住宅、ISP 和移动代理。创建免费账户。

创建免费账户
← 返回博客