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

几乎每个验证码集成都假设传入一个 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 在本地处理 验证码绕过 ,因此识别量不产生费用。
