在 Java 中将 CAPTCHA API 与 Selenium 配合使用

作者:solvethree阅读需 2 分钟

先请求令牌,然后在导航之前为 Chrome 配置相同的代理以及返回的 userAgent 和 secChUa。将 solution.token 放入应用的 CAPTCHA 字段,提交表单,并等待应用进入成功状态,而不只是等待任务就绪。

示例使用 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.api,而不是 Solve.main。

  3. 3.

    启用站点配置文件

    在 solvethree 控制台中设置目标主机名,并确保其配置文件已为工作区激活。发送主机名和操作不会创建配置文件。请参阅站点配置文件设置。

  4. 4.

    设置环境

    在将运行 BrowserSolve 的进程中配置以下三个环境变量。将工作区 API 密钥保留在受信任的 Java 进程中,不要放入页面或浏览器 JavaScript。

BrowserSolve 的环境变量
BrowserSolve 的环境变量
变量值
SOLVETHREE_API_KEY工作区 API 密钥。
SOLVETHREE_PROXY采用 IP 白名单的 HTTP 代理 URL,不含嵌入的用户名或密码、路径、查询参数或片段。
TARGET_URL包含表单的应用页面的完整 URL。

此示例需要采用 IP 白名单的代理,因为它不支持通过 Chrome 的代理 URL 传递代理凭据。任务 API 和 Chrome 使用相同的代理值。

添加 Solve.java 以发送 API 请求

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 在 browser.get(target) 之前通过 CDP 设置返回的 sec-ch-ua 请求头。不要先使用不相关的身份访问页面,直到提交令牌时才更改身份。

本示例特意只提交一个 proxy,这样 Chrome 就能使用同一个代理,而不必从候选代理中选择。请确保目标 URL 与任务使用的主机名和操作一致。

运行 BrowserSolve 前调整表单

浏览器示例面向您自有的表单,其中包含 CAPTCHA 响应字段、提交按钮和成功状态元素。在将 Maven 依赖加入类路径并运行 BrowserSolve.main 之前,请修改这些与应用相关的值。

  • action:将 login 替换为您的应用在此次提交中使用的操作。
  • g-recaptcha-response:确认表单实际提交的字段名。示例将 solution.token 赋给该字段,并派发一个冒泡的 input 事件。
  • button[type=submit]:如果此选择器无法定位目标表单的提交按钮,请替换它。
  • success:将此元素 ID 替换为仅在当前提交成功后才出现的状态元素。

设置字段并派发事件并不是通用的 CAPTCHA 回调方式。如果应用的表单逻辑不使用该字段或事件,请根据实际绑定方式调整提交步骤。检查发出的请求,确认其中包含的是 solution.token,而不是任务 ID 或旧令牌。

最后的 WebDriver 等待会检查应用的成功状态元素。请谨慎选择该标记:提交前就已存在的元素无法证明新请求的结果。任务就绪表示 solvethree 已生成解决结果;目标是否接受提交,则由应用响应来确认。

处理失败,避免不必要地创建新任务

API 返回非成功 HTTP 响应。

将 HTTP 响应与任务状态分开检查。Solve.api 会先抛出异常,而不会将其作为成功响应载荷处理。

试试这样做: 重试前先诊断请求错误。确认请求字段和已启用的站点配置,而不是反复发送未经修改的无效输入。

任务进入 failed 状态。

读取任务响应中的 errorCode 和 error。BrowserSolve 的通用异常不会显示这些字段,但 Solve.java 独立示例会显示。

试试这样做: 停止轮询该任务,并在错误处理中展示这些字段。解题失败会退款;不要认为所有失败都是额度问题。

创建超时或达到本地截止时间。

确认您是否已有任务 ID,或是否保存了原始请求体和 requestId。连接超时并不能证明创建失败。

试试这样做: 使用任务 ID 继续获取结果,或使用相同的 requestId 和完全相同的输入恢复创建。如果需要在重启后恢复,请在创建前保存请求体;重新运行提供的 main 方法会生成新的 requestId。

任务已就绪,但应用拒绝提交。

检查发出的令牌、操作、目标 URL、代理、用户代理和 sec-ch-ua。确认应用实际提交了新赋给字段的值。

试试这样做: 在付费再次解题前,先修正不匹配的值或提交绑定方式。将成功使用的代理与返回的身份配套使用。

180 秒的 API 截止时间和 20 秒的应用等待时间是示例选择的本地限制,并非服务保证。更多 API 错误详情,请参阅 CAPTCHA API 错误。

运行您的第一个任务。

使用 $5 工作区额度开始体验。

创建工作区
在 Java 中将 CAPTCHA API 与 Selenium 配合使用 · solvethree