Как решить GeeTest v3 в PHP и отправить результат обратно

Большинство кода для капчи предполагает один токен на входе и один на выходе. GeeTest в это не вписывается. Решение возвращает три значения, которые должны отправляться вместе, и переданная вами задача истекает примерно через шестьдесят секунд после выдачи. В PHP, где работа часто помещается в очередь, это истечение вызывает очень специфический и очень запутанный класс ошибок.
Два входных значения, ведущих себя по-разному
| Значение | Пожизненно |
|---|---|
gt | Статичен для каждого сайта. Можно кэшировать |
challenge | Одноразовый, недействителен примерно через 60 секунд |
Оба берутся из собственного init-эндпоинта GeeTest на сайте. 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 управляет только капчами с картинками. Всё интерактивное, включая GeeTest, использует 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'] тоже заполняется, но для GeeTest он содержит необработанную строку 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);Параметр для сброса кэша выполняет реальную работу. Init-эндпоинты GeeTest часто кэшируются CDN или обратным прокси, и кэшированный ответ выдаёт вам challenge, который кто-то уже использовал.
Эти три имени полей — обычное соглашение для GeeTest v3, но сайт может их переименовать, поэтому сначала проверьте реальную форму.
Ловушка очереди, которая ещё хуже в PHP
PHP-приложения сильно опираются на очереди задач, а GeeTest плохо сочетается с очевидным подходом. Постановка в очередь задачи, которая несёт gt и challenge выглядит разумно и даёт сбой, как только в очереди появляется затор:
// WRONG: the challenge expires while the job waits to be picked up. $init = fetchGeetestPair(); dispatch(new SolveCaptchaJob($init['gt'], $init['challenge'], $url));
Вместо этого получайте пару внутри воркера, чтобы к моменту использования challenge был возрастом в несколько секунд:
// 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
}Большинство сбоев GeeTest приходят в виде ApiException и означают, что challenge умер. Повторная попытка с той же парой никогда не удаётся, потому что израсходованный challenge не становится действительным снова.
Часто задаваемые вопросы
Почему $result[‘code’] нельзя использовать?
Потому что ответ — это три значения, а не одно. code сохраняет необработанный JSON для полноты, тогда как SDK разворачивает полезные части в challenge, validate и seccode. Отправьте эти три.
Можно ли хранить challenge в базе данных?
Нет. Он одноразовый и истекает примерно за минуту, поэтому всё, что его сохраняет, передаст мёртвое значение тому, кто его потом считает. Кэшируйте gt если хотите; но никогда сам challenge.
Охватывает ли это GeeTest v4?
The geetest метод нацелен на v3, слайдер-пазл, построенный на gt и challenge пара. v4 изменил модель параметров, поэтому сверьтесь с актуальной Документация по API прежде чем предполагать, что применяется тот же вызов.
Сводка
Получите пару с обходом кэша, решите немедленно, затем отправьте challenge, validate и seccode вместе. Используйте возвращённый challenge, кэшируйте только gt, и загружайте внутри воркера, а не передавайте challenge через очередь.
Другие языки — на Решение для GeeTest страница, более широкий набор возможностей PHP на Решатель капчи для PHP страница, и есть «живая» головоломка на нашей Демо GeeTest v3. CapSkip обрабатывает обход капчи локально, поэтому объём ничего не стоит за каждое решение.
