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

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

Большинство кода для капчи предполагает один токен на входе и один на выходе. 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 обрабатывает обход капчи локально, поэтому объём ничего не стоит за каждое решение.