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

大多数验证码代码假设传入一个 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 在本地处理 验证码绕过 在本地进行,因此识别量不产生任何按次费用。
