如何用 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, validate 和 seccode。提交那些。

我可以缓存 gt 值吗?

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

这处理极验 v4 吗?

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

小结

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

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