Java에서 Selenium으로 CAPTCHA API 사용하기

작성: solvethree읽는 데 4분

먼저 토큰을 요청한 다음, 페이지로 이동하기 전에 동일한 프록시와 반환된 userAgent, secChUa로 Chrome을 구성하세요. 애플리케이션의 CAPTCHA 필드에 solution.token을 넣고 폼을 제출한 후, 작업이 준비되는 것에 그치지 말고 애플리케이션의 성공 상태를 기다리세요.

예제는 Java REST 헬퍼와 Selenium ChromeDriver를 사용합니다. 자동 CAPTCHA 감지나 브라우저 플러그인은 사용하지 않습니다.

이 페이지의 내용

Java 프로젝트 및 대상 사이트 준비

  1. 1.

    프로젝트 구성

    Java 21 이상을 사용하세요. Maven 프로젝트에서 Java 컴파일 수준을 21로 설정하고 Gson 2.15.0과 org.seleniumhq.selenium:selenium-java:4.52.0을 추가하세요. Selenium Chrome 문서에 따라 Chrome과 ChromeDriver를 구성하세요.

  2. 2.

    두 Java 파일 저장

    첫 번째 예제는 src/main/java/Solve.java로, 두 번째 예제는 src/main/java/BrowserSolve.java로 저장하세요. 두 파일을 같은 패키지에 두세요. 제공된 파일에는 패키지 선언이 없습니다. BrowserSolve는 Solve.main이 아닌 Solve.api를 호출합니다.

  3. 3.

    사이트 프로필 활성화

    solvethree 콘솔에서 대상 호스트 이름을 설정하고 워크스페이스에서 해당 프로필이 활성화되어 있는지 확인하세요. 호스트 이름과 액션을 전송하는 것만으로는 프로필이 생성되지 않습니다. 사이트 프로필 설정을 참고하세요.

  4. 4.

    환경 설정

    BrowserSolve를 실행할 프로세스에 아래 세 가지 환경 변수를 구성하세요. 워크스페이스 API 키는 페이지나 브라우저 JavaScript가 아닌 신뢰할 수 있는 Java 프로세스에 보관하세요.

BrowserSolve 환경 변수
BrowserSolve 환경 변수
변수값
SOLVETHREE_API_KEY워크스페이스 API 키입니다.
SOLVETHREE_PROXY사용자 이름이나 비밀번호, 경로, 쿼리 또는 프래그먼트가 포함되지 않은 IP 허용 목록 기반 HTTP 프록시 URL입니다.
TARGET_URL폼이 포함된 애플리케이션 페이지의 전체 URL입니다.

이 예제는 Chrome의 프록시 URL을 통한 프록시 자격 증명 전달을 지원하지 않으므로 IP 허용 목록 기반 프록시가 필요합니다. 작업 API와 Chrome에 동일한 프록시 값을 전달합니다.

API 요청용 Solve.java 추가

Solve.api는 https://api.solvethree.com/v1에 인증된 요청을 보내고, 성공 응답을 파싱하기 전에 HTTP 상태를 확인하며, 각 요청의 시간 제한을 남은 로컬 기한으로 설정합니다.

Java 21+ · Solve.java (Gson 의존성)
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;

public class Solve {
    static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(15)).build();
    static final String KEY = System.getenv("SOLVETHREE_API_KEY");

    static JsonObject api(String path, JsonObject body, long deadline) throws Exception {
        long remaining = deadline - System.nanoTime();
        if (remaining <= 0) throw new IllegalStateException("Keep the task ID and resume polling");
        var builder = HttpRequest.newBuilder(URI.create("https://api.solvethree.com/v1" + path))
            .timeout(Duration.ofNanos(Math.min(remaining, Duration.ofSeconds(30).toNanos())))
            .header("Authorization", "Bearer " + KEY)
            .header("Content-Type", "application/json");
        if (body == null) builder.GET();
        else builder.POST(HttpRequest.BodyPublishers.ofString(body.toString()));
        var response = HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300)
            throw new IllegalStateException("API HTTP " + response.statusCode());
        return JsonParser.parseString(response.body()).getAsJsonObject();
    }
    public static void main(String[] args) throws Exception {
        String proxy = System.getenv("SOLVETHREE_PROXY");
        if (KEY == null || proxy == null) throw new IllegalArgumentException("Set the API key and proxy");
        var input = new JsonObject();
        input.addProperty("site", "accounts.example.com");
        input.addProperty("action", "login");
        input.addProperty("proxy", proxy);
        input.addProperty("requestId", UUID.randomUUID().toString());
        // Save input before creation if the task must be recovered after restart.
        long deadline = System.nanoTime() + Duration.ofSeconds(180).toNanos();
        var task = api("/tasks?waitMs=0", input, deadline);
        while (task.get("status").getAsString().equals("pending") || task.get("status").getAsString().equals("solving")) {
            String id = URLEncoder.encode(task.get("id").getAsString(), StandardCharsets.UTF_8);
            task = api("/tasks/" + id + "?waitMs=15000", null, deadline);
            if (task.get("status").getAsString().equals("pending") || task.get("status").getAsString().equals("solving"))
                Thread.sleep(1000);
        }
        if (!task.get("status").getAsString().equals("ready"))
            throw new IllegalStateException("Task failed: " + task.get("errorCode")
                + ": " + task.get("error"));
        var solution = task.getAsJsonObject("solution");
        // Submit solution.token with userAgent/secChUa and the original proxy.
        System.out.println("Ready task: " + task.get("id").getAsString());
    }
}

독립 실행형 main 메서드는 작업 생성과 폴링을 보여 줍니다. 여기에 사용된 accounts.example.com 호스트 이름과 login 액션은 자리표시자입니다. Selenium 통합에서는 이 파일을 헬퍼로 사용하고 대신 BrowserSolve를 실행하세요. 두 main 메서드를 모두 실행하면 별도의 작업이 생성됩니다.

reCAPTCHA 요청에는 site의 호스트 이름, 애플리케이션의 action, 그리고 proxy 또는 proxyCandidates 중 정확히 하나가 필요합니다. type이나 url은 추가하지 마세요. recaptchaType을 생략하면 이 예제에서 사용하는 enterprise-v3가 선택됩니다.

준비된 솔루션의 식별 정보로 Chrome 시작

BrowserSolve는 TARGET_URL에서 site를 도출하고, 작업을 생성한 다음 상태가 pending 또는 solving인 동안 폴링합니다. 브라우저에서 사용할 솔루션은 ready 상태의 작업에서만 제공됩니다.

Java · 준비된 솔루션을 Chrome에 전달
// Save as BrowserSolve.java beside Solve.java from the Java REST quickstart.
// Add Selenium Java 4.52.0 and Gson 2.15.0 to your Maven dependencies; Java 21+.
import com.google.gson.JsonObject;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class BrowserSolve {
    public static void main(String[] args) throws Exception {
        String target = System.getenv("TARGET_URL");
        String proxy = System.getenv("SOLVETHREE_PROXY");
        URI proxyUri = URI.create(proxy);
        if (proxyUri.getUserInfo() != null) throw new IllegalArgumentException("Use an IP-allowlisted proxy");
        var input = new JsonObject();
        input.addProperty("site", URI.create(target).getHost());
        input.addProperty("action", "login");
        input.addProperty("proxy", proxy);
        input.addProperty("requestId", UUID.randomUUID().toString());
        long deadline = System.nanoTime() + Duration.ofSeconds(180).toNanos();
        var task = Solve.api("/tasks?waitMs=0", input, deadline);
        while (task.get("status").getAsString().matches("pending|solving")) {
            String id = URLEncoder.encode(task.get("id").getAsString(), StandardCharsets.UTF_8);
            task = Solve.api("/tasks/" + id + "?waitMs=15000", null, deadline);
            if (task.get("status").getAsString().matches("pending|solving")) Thread.sleep(1000);
        }
        if (!task.get("status").getAsString().equals("ready")) throw new IllegalStateException("Task failed");
        var solution = task.getAsJsonObject("solution");
        var options = new ChromeOptions();
        options.addArguments("--proxy-server=" + proxy, "--user-agent=" + solution.get("userAgent").getAsString());
        var browser = new ChromeDriver(options);
        try {
            browser.executeCdpCommand("Network.enable", Map.of());
            browser.executeCdpCommand("Network.setExtraHTTPHeaders", Map.of("headers",
                Map.of("sec-ch-ua", solution.get("secChUa").getAsString())));
            browser.get(target);
            var wait = new WebDriverWait(browser, Duration.ofSeconds(20));
            var field = wait.until(driver -> driver.findElement(By.name("g-recaptcha-response")));
            browser.executeScript("arguments[0].value=arguments[1]; arguments[0].dispatchEvent(new Event('input',{bubbles:true}));",
                field, solution.get("token").getAsString());
            browser.findElement(By.cssSelector("button[type=submit]")).click();
            wait.until(driver -> driver.findElement(By.id("success"))); // Your application's success marker.
        } finally { browser.quit(); }
    }
}

순서가 중요합니다. ChromeOptions에서 프록시와 반환된 사용자 에이전트를 설정한 다음, ChromeDriver에서 CDP를 통해 반환된 sec-ch-ua 헤더를 설정하고 나서 browser.get(target)을 호출합니다. 관련 없는 식별 정보로 먼저 페이지를 연 뒤 토큰을 제출할 때만 식별 정보를 바꾸지 마세요.

이 예제는 Chrome에서 후보를 선택할 필요 없이 동일한 프록시를 사용하도록 의도적으로 proxy 하나만 제출합니다. 대상 URL은 작업에 사용한 호스트 이름 및 액션과 일치해야 합니다.

BrowserSolve 실행 전 폼 수정

브라우저 예제는 CAPTCHA 응답 필드, 제출 버튼, 성공 요소가 있는 직접 소유한 폼을 대상으로 합니다. Maven 의존성을 클래스 경로에 포함해 BrowserSolve.main을 실행하기 전에 애플리케이션별로 다른 이 값들을 수정하세요.

  • action: login을 이 제출에 애플리케이션이 사용하는 액션으로 바꾸세요.
  • g-recaptcha-response: 폼이 실제로 제출하는 필드 이름을 확인하세요. 예제는 solution.token을 할당하고 버블링되는 input 이벤트를 발생시킵니다.
  • button[type=submit]: 이 선택자가 의도한 폼의 제출 버튼을 가리키지 않으면 교체하세요.
  • success: 이 요소 ID를 현재 제출이 성공한 후에만 나타나는 상태를 가리키는 값으로 바꾸세요.

필드 값을 설정하고 이벤트를 발생시키는 방식이 모든 CAPTCHA에 통용되는 콜백은 아닙니다. 애플리케이션의 폼 로직이 해당 필드나 이벤트를 사용하지 않는다면 실제 연결 방식에 맞게 제출 단계를 수정하세요. 전송되는 요청을 검사해 작업 ID나 이전 토큰이 아닌 solution.token이 포함되어 있는지 확인하세요.

마지막 WebDriver 대기는 애플리케이션의 성공 요소를 확인합니다. 이 표시는 신중하게 선택하세요. 제출 전부터 있던 요소는 새 요청의 성공 여부를 입증하지 못합니다. 작업이 준비되었다는 것은 solvethree가 솔루션을 생성했다는 뜻이며, 대상의 수락 여부는 애플리케이션 응답으로 확인합니다.

불필요한 추가 작업 없이 실패 처리

API가 성공이 아닌 HTTP 응답을 반환합니다.

HTTP 응답은 작업 상태와 별도로 확인하세요. Solve.api는 이를 성공 페이로드로 처리하기 전에 예외를 발생시킵니다.

다음을 시도해 보세요: 재시도하기 전에 요청 오류의 원인을 파악하세요. 잘못된 입력을 수정 없이 반복 전송하지 말고 요청 필드와 활성 사이트 프로필을 확인하세요.

작업이 failed 상태가 됩니다.

작업 응답에서 errorCode와 error를 읽으세요. Solve.java 독립 실행형 예제는 이 값을 표시하지만 BrowserSolve의 일반 예외는 표시하지 않습니다.

다음을 시도해 보세요: 해당 작업의 폴링을 중단하고 오류 처리 과정에서 이 필드들을 노출하세요. 실패한 해결 작업은 환불됩니다. 모든 실패가 크레딧 문제라고 가정하지 마세요.

생성 요청이 시간 초과되거나 로컬 제한 시간이 만료됩니다.

이미 작업 ID가 있는지, 또는 원래 요청 본문과 requestId를 저장했는지 확인하세요. 연결 시간 초과만으로 생성 실패가 확정되지는 않습니다.

다음을 시도해 보세요: 작업 ID로 조회를 재개하거나 동일한 requestId와 동일한 입력으로 생성 요청을 복구하세요. 재시작 후에도 복구해야 한다면 생성 전에 본문을 저장하세요. 제공된 main 메서드를 다시 실행하면 새 requestId가 생성됩니다.

작업은 준비되었지만 애플리케이션이 제출을 거부합니다.

전송되는 토큰, 액션, 대상 URL, 프록시, 사용자 에이전트, sec-ch-ua를 검사하세요. 애플리케이션이 새로 할당된 필드 값을 실제로 제출했는지 확인하세요.

다음을 시도해 보세요: 다른 해결 작업에 비용을 지불하기 전에 일치하지 않는 값이나 제출 연결 방식을 수정하세요. 성공한 프록시와 반환된 식별 정보는 함께 유지하세요.

180초의 API 제한 시간과 20초의 애플리케이션 대기 시간은 예제에서 정한 로컬 제한이며 서비스 보장이 아닙니다. 추가 API 오류 정보는 CAPTCHA API 오류를 참고하세요.

첫 작업을 실행하세요.

워크스페이스 크레딧 $5로 시작하세요.

워크스페이스 만들기
Java에서 Selenium으로 CAPTCHA API 사용하기 · solvethree