如何用 PHP 识别极验 v3 并回传结果

solve geetest in php - How to Solve GeeTest v3 in PHP and Post the Result Back

大多数验证码代码假设传入一个 token、传出一个 token。极验不符合这一点。一次识别返回 三个 这些值必须一起提交,而你传入的挑战大约在签发六十秒后就会过期。在 PHP 中,工作往往会被推入队列,这种过期会引发一类非常特殊、也非常令人困惑的 bug。

两个行为不同的输入

值有效期
gt对每个站点是静态的。可安全缓存
challenge一次性,约 60 秒后失效

两者都来自站点自己的极验初始化端点。 gt 标识站点; challenge 是按次的,会很快失效。

设置

# PHP 8.0+, with the curl and json extensions.
composer require capskip/capskip
use CapSkip\CapSkip;

$solver = new CapSkip([
    'host' => '127.0.0.1',
    'port' => 8080,
    'recaptchaTimeout' => 300,   // GeeTest uses this, not defaultTimeout
]);

defaultTimeout 仅管辖图片验证码。所有交互式类型(包括极验)都使用 recaptchaTimeout.

识别

$result = $solver->geetest(
    '81388ea1fc187e0c335c0a8907ff2625',   // gt, static per site
    '7cf6a8b1a2c34d5e6f7089abcdef0123',   // challenge, fetched seconds ago
    'https://example.com/login'
);

echo $result['challenge'];
echo $result['validate'];
echo $result['seccode'];

那三个键就是答案。 $result['code'] 也会被填充,但对极验来说它保存的是原始 JSON 字符串而非任何可提交的内容,因此出于习惯去取它会产生令人困惑的失败。

使用 challenge 返回的那个,而不是你传入的那个。它们并不总是相同。

完整流程

use CapSkip\CapSkip;

$solver = new CapSkip();
$login = 'https://example.com/login';

// 1. Fresh pair, cache-busted. Cached init responses return spent challenges.
$init = json_decode(file_get_contents(
    'https://example.com/geetest/init?t=' . (int) (microtime(true) * 1000)
), true);

// 2. Solve immediately. Nothing slow between here and step 1.
$result = $solver->geetest($init['gt'], $init['challenge'], $login);

// 3. Post all three together with the real form fields.
$ch = curl_init($login);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'geetest_challenge' => $result['challenge'],
        'geetest_validate' => $result['validate'],
        'geetest_seccode' => $result['seccode'],
        'username' => '...',
        'password' => '...',
    ]),
]);
$response = curl_exec($ch);
curl_close($ch);

缓存破坏参数确实在起作用。极验初始化端点常被 CDN 或反向代理缓存,缓存的响应会给你一个别人已消费的挑战。

那三个字段名是常见的极验 v3 约定,但站点可以重命名它们,因此请先检查真实表单。

队列陷阱,在 PHP 中更严重

PHP 应用大量依赖任务队列,而极验与这种显而易见的设计配合得很糟。派发一个携带 gt 和 challenge 的任务看起来合理,但只要队列有任何积压就会失败:

// WRONG: the challenge expires while the job waits to be picked up.
$init = fetchGeetestPair();
dispatch(new SolveCaptchaJob($init['gt'], $init['challenge'], $url));

改为在工作进程内部获取参数对,使挑战在使用时只有几秒钟:

// RIGHT: the job fetches its own pair at the moment it runs.
class SolveCaptchaJob
{
    public function handle(CapSkip $solver): array
    {
        $init = fetchGeetestPair();   // called inside the worker
        return $solver->geetest($init['gt'], $init['challenge'], $this->url);
    }
}

PHP 同步执行,而 AsyncCapSkip 在这里只是一个别名,因此并行来自运行更多工作进程,而非来自 SDK。

错误

use CapSkip\Exceptions\ValidationException;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\TimeoutException;

try {
    $result = $solver->geetest($gt, $challenge, $pageUrl);
} catch (ValidationException $e) {
    // missing gt or challenge
} catch (NetworkException $e) {
    // CapSkip is not running
} catch (ApiException $e) {
    // usually a challenge that expired before the solve finished
} catch (TimeoutException $e) {
    // exceeded recaptchaTimeout
}

大多数极验失败会以 ApiException 形式出现,意味着挑战已失效。用相同的参数对重试永远不会成功,因为已消费的挑战不会再次生效。

常见问题

为什么 $result['code'] 不可用?

因为答案是三个值而非一个。 code 保留原始 JSON 以求完整,而 SDK 将有用的部分展开到 challenge, validate 和 seccode。回传那三个。

我可以把挑战存进数据库吗?

不可以。它是一次性的,约一分钟后过期,因此任何持久化它的做法都会把一个失效的值交给读取它的一方。你愿意的话可以缓存 gt ;但绝不要缓存挑战。

这涵盖极验 v4 吗?

加载的 geetest 方法针对 v3,即基于 gt 和 challenge 参数对构建的滑块拼图版本。v4 改变了参数模型,因此在假设同一调用可用之前,请查看当前的 API 文档 在假设同一调用适用之前请检查。

小结

获取一个缓存破坏的参数对,立即识别,然后把 challenge, validate 和 seccode 一起回传。使用返回的 challenge,只缓存 gt,并在工作进程内部获取,而不是通过队列传递挑战。

其他语言见 GeeTest 识别工具 页面,更广泛的 PHP 用法见 PHP 验证码识别 页面,我们的 GeeTest v3 演示上尝试一个实时拼图。CapSkip 在本地处理 验证码绕过 在本地进行,因此识别量不产生任何按次费用。