如何用 Node.js 识别极验 v3 并回传

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

几乎每个验证码集成都假设传入一个 token、传出一个 token。极验并非如此。一次识别返回 三个 必须一起提交的值,而且你所依据的挑战在你获取后约一分钟过期。两种失败都不会产生有用的错误信息,这正是极验以难缠著称的原因。

两个输入,其中一个易腐

有效期
gt对每个站点是静态的。可自由缓存
challenge一次性,约 60 秒后失效

两者都来自站点自己的极验初始化端点。把挑战当作可重用,正是集成在测试中通过、在生产中崩溃的原因——那里排队延迟会把识别推过过期时间。

设置

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 也会被填充,但对极验来说它保存的是原始 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: '...',
  }),
});

缓存破坏时间戳确实在起作用。极验初始化端点经常被 CDN 或代理缓存,缓存的响应会给你一个别人已经用过的挑战。

那些字段名是常见的极验 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;
}

大多数极验失败会以 ApiException 形式出现,意味着挑战已过期。用相同的参数对重试永远无效。请获取一个新的。

常见问题

为什么我不能直接用 result.code?

因为极验的答案是三个值。 code 保留原始 JSON 以求完整,而 SDK 将有用的部分展开到 challenge, validateseccode。提交那些。

我可以缓存 gt 值吗?

可以。 gt 是站点标识符,很少改变。 challenge 是易腐的那一半,每次都必须新获取。

这处理极验 v4 吗?

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

小结

获取一个缓存破坏的参数对,立即识别,然后把 challenge, validateseccode 一起回传。使用返回的 challenge,只缓存 gt,并在并行运行时把获取和识别放在同一任务中。

其他语言见 GeeTest 识别工具 页面,更广泛的 Node 用法见 Node.js 验证码识别 页面,我们的 GeeTest v3 演示上也有一个实时拼图。CapSkip 在本地处理 验证码绕过 ,因此识别量不产生费用。