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

solve geetest in node.js - How to Solve GeeTest v3 in Node.js and Post It Back

Почти каждая интеграция капчи предполагает, что один токен входит и один выходит. GeeTest работает не так. Решение возвращает три значения, которые нужно отправлять вместе, а challenge, с которого вы начали, истекает примерно через минуту после получения. Ни одна из этих ошибок не выдаёт внятного сообщения, поэтому GeeTest и славится своей капризностью.

Два входных значения, одно из них недолговечное

ЗначениеПожизненно
gtСтатично для каждого сайта. Кэшируйте свободно
challengeОдноразовый, умирает примерно за 60 секунд

Оба берутся из собственного init-эндпоинта GeeTest на сайте. Отношение к challenge как к многоразовому — это причина, по которой интеграции проходят тесты и рушатся в продакшене, где задержка в очереди сдвигает решение за пределы срока действия.

Настройка

npm install capskip
const { CapSkip } = require('capskip');

const solver = new CapSkip({
  host: '127.0.0.1',
  port: 8080,
  recaptchaTimeout: 300,   // GeeTest uses this, not defaultTimeout
});

Это различие имеет значение. defaultTimeout управляет только капчами-картинками; всё интерактивное использует recaptchaTimeout.

Распознавание

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

console.log(result.challenge, result.validate, result.seccode);

Эти три свойства и есть ответ. result.code тоже заполняется, но для GeeTest он содержит необработанную строку JSON, а не что-то пригодное для отправки, поэтому обращение к нему по привычке приводит к загадочному сбою.

Используйте challenge который возвращается, а не тот, что вы передали. Они не всегда идентичны.

Весь процесс

const { CapSkip } = require('capskip');

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

// 1. Fresh pair, cache-busted. Cached init responses return spent challenges.
const init = await (await fetch(
  `https://example.com/geetest/init?t=${Date.now()}`,
)).json();

// 2. Solve immediately. Nothing slow between here and the previous step.
const result = await solver.geetest(init.gt, init.challenge, LOGIN);

// 3. Post all three together with the real form fields.
const response = await fetch(LOGIN, {
  method: 'POST',
  body: new URLSearchParams({
    geetest_challenge: result.challenge,
    geetest_validate: result.validate,
    geetest_seccode: result.seccode,
    username: '...',
    password: '...',
  }),
});

Временная метка для обхода кэша делает реальную работу. Эндпоинты инициализации GeeTest часто кэшируются CDN или прокси, и кэшированный ответ выдаёт вам капчу, которую кто-то уже использовал.

Эти имена полей — обычное соглашение GeeTest v3, хотя сайт может их переименовать. Проверьте реальную форму, прежде чем делать предположения.

Ловушка пакетной обработки

Именно здесь эргономика Node работает против вас. Очевидная параллельная версия неверна:

// WRONG: every challenge is fetched up front, so the later solves
// start against pairs that have already expired.
const pairs = await Promise.all(urls.map(fetchPair));
const results = await Promise.all(
  pairs.map((p, i) => solver.geetest(p.gt, p.challenge, urls[i])));

Держите получение и решение внутри одной задачи, чтобы каждая пара использовалась в течение секунд после выдачи:

// RIGHT: each task fetches its own pair immediately before solving.
const results = await Promise.all(urls.map(async (url) => {
  const pair = await fetchPair(url);
  return solver.geetest(pair.gt, pair.challenge, url);
}));

AsyncCapSkip экспортируется как псевдоним для CapSkip здесь, поэтому отдельного асинхронного клиента искать не нужно. Promise.all — это вся суть.

Ошибки

const {
  ValidationException, NetworkException, ApiException, TimeoutException,
} = require('capskip');

try {
  const result = await solver.geetest(gt, challenge, pageUrl);
} catch (err) {
  if (err instanceof ValidationException)   { /* missing gt or challenge */ }
  else if (err instanceof NetworkException) { /* CapSkip not running */ }
  else if (err instanceof ApiException)     { /* usually an expired challenge */ }
  else if (err instanceof TimeoutException) { /* exceeded recaptchaTimeout */ }
  else throw err;
}

Большинство сбоев GeeTest приходят в виде ApiException и означают, что challenge истёк. Повторная попытка с той же парой никогда не работает. Запросите новый.

Часто задаваемые вопросы

Почему нельзя просто использовать result.code?

Потому что ответ GeeTest — это три значения. code содержит необработанный JSON для полноты, тогда как SDK раскрывает полезные части в challenge, validate и seccode. Отправьте их.

Можно ли кэшировать значение gt?

Да. gt — это идентификатор сайта, который редко меняется. challenge — это скоропортящаяся половина, и её нужно получать заново каждый раз.

Обрабатывает ли это GeeTest v4?

The geetest метод нацелен на v3, слайдер-пазл, построенный на gt и challenge пара. v4 изменил модель параметров, поэтому сверьтесь с актуальной Документация по API прежде чем предполагать, что тот же вызов сработает.

Сводка

Получите пару с обходом кэша, решите немедленно, затем отправьте challenge, validate и seccode вместе. Используйте возвращённый challenge, кэшируйте только gt, и держите получение и решение внутри одной задачи при параллельном выполнении.

Другие языки — на Решение для GeeTest странице, более широкий набор Node на Сервис распознавания капчи для Node.js страница, и есть «живая» головоломка на нашей Демо GeeTest v3. CapSkip умеет обход капчи локально, поэтому объём решений ничего не стоит.