如何用 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 应用大量依赖任务队列,而极验与这种显而易见的设计配合得很糟。派发一个携带 gtchallenge 的任务看起来合理,但只要队列有任何积压就会失败:

// 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, validateseccode。回传那三个。

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

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

这涵盖极验 v4 吗?

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

小结

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

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