Kotlinでのプロキシ利用:Ktor ClientとOkHttpの実装ガイド

Kotlinでのプロキシ利用を完全解説。Ktor ClientとOkHttpでProxyHatのレジデンシャルプロキシを設定し、geo-targeting、sticky session、SOCKS5、並行リクエスト、407認証対応まで実装例付きで解説します。

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp

Kotlinでのプロキシ利用の基礎と実装パターン

AndroidアプリやKotlinバックエンドでHTTPリクエストを送信する際、IPブロックやレート制限、地理的制約に直面することは珍しくありません。Kotlinでのプロキシ利用は、これらの制限を回避し、信頼性の高いデータ取得を実現するための実用的なアプローチです。本記事では、Ktor ClientとOkHttpの両方でプロキシを設定し、ProxyHatのレジデンシャルプロキシを経由してリクエストをルーティングする方法を、実行可能なコードとともに解説します。

なぜKotlinプロジェクトでプロキシが必要なのか

モバイルアプリやサーバーサイドのKotlinアプリケーションが直接ターゲットサイトにアクセスすると、送信元IPはクラウドプロバイダーのデータセンターASNに分類されます。多くのプラットフォームは、AWSやGCPのIPレンジからの異常なリクエストを自動的に検知し、CAPTCHAや403エラーで応答します。レジデンシャルプロキシを使用すると、リクエストが実際のISPに割り当てられたIPアドレスから送信されているように見え、ブロックされる確率が大幅に下がります。

HTTPプロキシ認証は MDNのドキュメント で規定されている通り、Proxy-AuthorizationヘッダーでBasic認証クレデンシャルを送信する仕組みです。しかし、KtorとOkHttpでこのヘッダーを設定する方法はエンジン依存であり、それぞれ異なるアプローチが必要です。

プロジェクトセットアップ — Ktor 3とOkHttp

まずは依存関係を設定します。Ktor 3.xのCIOエンジンとOkHttpエンジンの両方をプロジェクトに追加し、OkHttp単体でもプロキシを利用できるようにします。

// build.gradle.kts
dependencies {
    // Ktor Client 3.x
    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("io.ktor:ktor-client-content-negotiation:3.0.3")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")

    // OkHttp (standalone)
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

OkHttpのベースライン設定

最もシンプルな出発点は、OkHttpClient.Builder().proxy()java.net.Proxyを指定することです。OkHttpの公式ドキュメント にある通り、OkHttpは標準的なJavaプロキシAPIをサポートしています。

import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Authenticator
import java.net.PasswordAuthentication

fun createOkHttpClientWithProxy(): OkHttpClient {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))

    // プロキシ認証をjava.net.Authenticatorで設定
    Authenticator.setDefault(object : Authenticator() {
        override fun getPasswordAuthentication(): PasswordAuthentication {
            return PasswordAuthentication("user-country-DE", "pass".toCharArray())
        }
    })

    return OkHttpClient.Builder()
        .proxy(proxy)
        .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .build()
}

fun main() {
    val client = createOkHttpClientWithProxy()
    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println(response.body?.string())
        // {"origin": "xxx.xxx.xxx.xxx"} — ドイツのレジデンシャルIPが表示される
    }
}

Ktor Clientの設定 — CIOエンジン

Ktorでは、エンジンによってプロキシのサポート方法が異なります。CIOエンジンはネイティブのプロキシサポートを提供しませんが、システムプロパティでプロキシホストを指定し、defaultRequestブロックでProxy-Authorizationヘッダーを明示的に追加すれば認証付きプロキシを利用可能です。

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking
import java.util.Base64

fun createKtorClientWithProxy(): HttpClient {
    val proxyUser = "user-country-DE-city-berlin"
    val proxyPass = "pass"
    val authHeader = "Basic " + Base64.getEncoder()
        .encodeToString("$proxyUser:$proxyPass".toByteArray())

    return HttpClient(CIO) {
        engine {
            // CIOエンジンではプロキシホスト/ポートをシステムプロパティで指定
            System.setProperty("http.proxyHost", "gate.proxyhat.com")
            System.setProperty("http.proxyPort", "8080")
            System.setProperty("https.proxyHost", "gate.proxyhat.com")
            System.setProperty("https.proxyPort", "8080")
        }

        defaultRequest {
            // プロキシ認証ヘッダーを明示的に追加
            header("Proxy-Authorization", authHeader)
            url("https://httpbin.org/")
        }

        engineConfig {
            requestTimeout = 30000
            endpoint {
                connectTimeout = 10000
                connectAttempts = 3
            }
        }
    }
}

fun main() = runBlocking {
    val client = createKtorClientWithProxy()
    try {
        val response: String = client.get("/ip").body()
        println(response)
    } finally {
        client.close()
    }
}

Ktor Client — OkHttpエンジン

OkHttpエンジンを使用する場合は、エンジン設定内で直接プロキシと認証を指定できます。この方がエンジン依存の問題が少なく、プロダクション環境で推奨されます。

import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import java.net.InetSocketAddress
import java.net.Proxy

fun createKtorOkHttpClient(): HttpClient {
    return HttpClient(OkHttp) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
            config {
                // OkHttpのネイティブプロキシ認証を使用
                proxyAuthenticator = okhttp3.Authenticator { _, response ->
                    val credential = okhttp3.Credentials.basic("user-country-DE", "pass")
                    response.request.newBuilder()
                        .header("Proxy-Authorization", credential)
                        .build()
                }
                connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
            }
        }
    }
}

fun main() = runBlocking {
    val client = createKtorOkHttpClient()
    try {
        val response: String = client.get("https://httpbin.org/ip").body()
        println(response)
    } finally {
        client.close()
    }
}

ProxyHatのルーティング — geo-targetingとsticky session

ProxyHatでは、geo-targetingとsticky sessionのパラメータをユーザー名にエンコードします。これにより、追加のAPIコールなしでIPの国・都市を指定したり、同じIPを維持したりできます。

  • 国指定: user-country-US
  • 都市指定: user-country-DE-city-berlin
  • Sticky session: user-session-abc123
  • 組み合わせ: user-country-DE-city-berlin-session-abc123

ProxyHatのSDKは、このパターンを内部的にミラーリングしています。SDKを使用しない場合は、上記のコード例のようにユーザー名文字列を直接構築してください。詳細は ProxyHatドキュメント を参照してください。

SOCKS5プロキシの利用 — ポート1080

SOCKS5プロキシを使用する場合は、ポート1080を使用し、Javaのシステムプロパティでユーザー名とパスワードを設定します。SOCKS5はHTTPプロキシよりも低レベルで動作し、TCP接続全体をトンネルするため、HTTP以外のプロトコルやWebSocketでも利用できます。

import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy

fun createSocks5Client(): OkHttpClient {
    // SOCKS5認証情報をシステムプロパティに設定
    System.setProperty("java.net.socks.username", "user-country-US")
    System.setProperty("java.net.socks.password", "pass")

    val socksProxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080))

    return OkHttpClient.Builder()
        .proxy(socksProxy)
        .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .build()
}

fun main() {
    val client = createSocks5Client()
    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println(response.body?.string())
    }
}

注意点として、java.net.socks.username/passwordはJVM全体のシステムプロパティであるため、複数のプロキシを同時に使用する場合はスレッドセーフな設計が必要です。その場合は、HTTPプロキシ(ポート8080)とProxy-Authorizationヘッダーを使用する方が柔軟性が高いでしょう。

レジデンシャルプロキシが必要なケース — 並行リクエストの実装

ソーシャルメディアプラットフォームやeコマースサイトは、データセンターASNからのリクエストを高度なフィンガープリントングで検知します。レジデンシャルプロキシを使用すると、ISPに割り当てられた実際のIPアドレスからリクエストが送信されるため、ブロックされる確率が大幅に低下します。以下は、Kotlinコルーチンで複数のリクエストを並行実行し、Semaphoreで同時接続数を制御する例です。

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Authenticator
import java.net.PasswordAuthentication
import java.util.concurrent.TimeUnit

suspend fun fanOutRequests(
    urls: List<String>,
    proxyUser: String,
    proxyPass: String,
    maxConcurrency: Int = 10
): List<Result<String>> = coroutineScope {
    val semaphore = Semaphore(maxConcurrency)

    // プロキシ認証を設定
    Authenticator.setDefault(object : Authenticator() {
        override fun getPasswordAuthentication(): PasswordAuthentication {
            return PasswordAuthentication(proxyUser, proxyPass.toCharArray())
        }
    })

    val client = OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    // 並行リクエストをファンアウト
    val deferreds = urls.map { url ->
        async(Dispatchers.IO) {
            semaphore.withPermit {
                try {
                    val request = Request.Builder().url(url).build()
                    client.newCall(request).execute().use { response ->
                        if (response.isSuccessful) {
                            Result.success(response.body?.string() ?: "")
                        } else {
                            Result.failure(RuntimeException("HTTP ${response.code}"))
                        }
                    }
                } catch (e: Exception) {
                    Result.failure(e)
                }
            }
        }
    }

    deferreds.awaitAll()
}

fun main() = runBlocking {
    val urls = (1..50).map { "https://httpbin.org/ip?n=$it" }

    // 各リクエストで異なるsticky sessionを使用してIPを分散
    val results = fanOutRequests(
        urls = urls,
        proxyUser = "user-country-US-session-${System.currentTimeMillis()}",
        proxyPass = "pass",
        maxConcurrency = 10
    )

    results.forEachIndexed { index, result ->
        result.onSuccess { println("[$index] OK: ${it.take(80)}") }
            .onFailure { println("[$index] FAIL: ${it.message}") }
    }
}

Semaphoreで同時接続数を10に制限することで、ターゲットサーバーへの負荷を抑えつつ、50件のリクエストを効率的に処理できます。各リクエストで異なるsessionIDを使用することで、ProxyHatが異なるレジデンシャルIPを割り当て、IP単位のレート制限を回避します。このパターンは WebスクレイピングSERP追跡 で特に有効です。

プロキシタイプの比較

特徴 レジデンシャル データセンター モバイル
IPソース ISPの実際のIP クラウドプロバイダーIP 携帯キャリアのIP
ブロック耐性 最高
平均レイテンシ 200ms〜500ms 50ms〜100ms 500ms〜1500ms
適した用途 Webスクレイピング、SERP追跡 APIアクセス、CI/CD ソーシャルメディア自動化

用途に応じたプロキシタイプの選択は、成功率とコストのバランスに直結します。詳細な 料金体系対応ロケーション は各ページを参照してください。

プロダクション環境での堅牢化

OkHttp Authenticatorで407チャレンジを処理

プロキシが407 Proxy Authentication Requiredを返した場合、OkHttpのAuthenticatorインターフェースで自動的に再認証できます。これにより、認証トークンの期限切れやプロキシのフェイルオーバーに対応できます。

import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

class ProxyAuthenticator(
    private val proxyUser: String,
    private val proxyPass: String
) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.code == 407) {
            val credential = Credentials.basic(proxyUser, proxyPass)
            return response.request.newBuilder()
                .header("Proxy-Authorization", credential)
                .build()
        }
        return null // 認証不可の場合はnullを返して再試行を停止
    }
}

fun createProductionClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .proxyAuthenticator(ProxyAuthenticator("user-country-DE-city-berlin", "pass"))
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .connectionPool(okhttp3.ConnectionPool(20, 5, TimeUnit.MINUTES))
        .build()
}

リトライとタイムアウトの戦略

プロダクション環境では、ネットワークの不安定性やプロキシの一時的な障害を想定し、リトライロジックを実装する必要があります。Kotlinでは、指数バックオフを組み合わせたリトライ関数をシンプルに実装できます。

import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.math.min
import kotlin.random.Random

suspend fun <T> retryWithBackoff(
    maxRetries: Int = 3,
    initialDelayMs: Long = 500,
    maxDelayMs: Long = 5000,
    block: suspend (attempt: Int) -> T?
): T? {
    var attempt = 0
    while (attempt < maxRetries) {
        try {
            val result = withTimeoutOrNull(30000L) { block(attempt) }
            if (result != null) return result
        } catch (e: Exception) {
            println("Attempt ${attempt + 1} failed: ${e.message}")
        }
        val delayMs = min(
            initialDelayMs * (1L << attempt) + Random.nextLong(0, 250),
            maxDelayMs
        )
        delay(delayMs)
        attempt++
    }
    return null
}

// 使用例
suspend fun fetchWithRetry(client: okhttp3.OkHttpClient, url: String): String? {
    return retryWithBackoff { _ ->
        val request = okhttp3.Request.Builder().url(url).build()
        client.newCall(request).execute().use { response ->
            if (response.isSuccessful) response.body?.string() else null
        }
    }
}

TLS設定とAndroid NetworkSecurityConfig

Androidアプリでプロキシを使用する場合、network_security_config.xmlでプロキシの通信を許可する必要があります。特に、cleartext traffic(HTTP)をプロキシ経由で送信する場合は、ドメインごとの許可設定が必要です。

<!-- 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"
    ... >
    ...
</application>

TLS 1.2以上を使用し、証明書のピン留めが必要な場合は、OkHttpのCertificatePinnerを活用できます。ただし、プロキシ経由の通信ではMITMのリスクがあるため、エンドツーエンドのTLS検証を維持することが重要です。接続プールサイズは20接続、タイムアウトは5分に設定することで、プロキシ接続の再利用率を最大化できます。

倫理的スクレイピングのベストプラクティス

プロキシを使用するWebスクレイピングは強力なツールですが、法的・倫理的ガイドラインを遵守することが不可欠です。

  • 公開データのみを取得: ログイン背後のデータや有料コンテンツのスクレイピングは避けてください。
  • robots.txtを尊重: ターゲットサイトのrobots.txtで禁止されているパスはスクレイピングしないでください。
  • 利用規約を確認: 米国では CFAA(Computer Fraud and Abuse Act)が不正アクセスを規定しており、EUでは GDPR が個人データの処理を制限しています。
  • 公式APIを優先: 多くのプラットフォームは公式APIを提供しており、スクレイピングよりも信頼性が高く、法的リスクも低いです。
  • レート制限を設ける: Semaphoreや遅延を用いて、ターゲットサーバーに過度な負荷をかけないようにしてください。

Key Takeaways

  • KtorのCIOエンジンではシステムプロパティとProxy-Authorizationヘッダーでプロキシを設定し、OkHttpエンジンではproxyproxyAuthenticatorを使用する。
  • ProxyHatのgeo-targetingとsticky sessionはユーザー名にエンコードする(例:user-country-DE-city-berlin-session-abc123)。
  • SOCKS5はポート1080を使用し、java.net.socks.username/passwordシステムプロパティで認証する。
  • レジデンシャルプロキシはデータセンターASNがブロックされるアプリ・ソーシャルターゲットに必須。コルーチンのasync/awaitAllSemaphoreで並行リクエストを制御する。
  • プロダクションでは407対応のAuthenticator、指数バックオフのリトライ、適切なタイムアウトとコネクションプールを設定する。
  • 公開データのみを取得し、robots.txtと利用規約を尊重し、公式APIを優先する。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る