如何用 Node.js 和 TypeScript 识别 Cloudflare Turnstile

solve cloudflare turnstile in node.js - How to Solve Cloudflare Turnstile in Node.js and TypeScript

Cloudflare Turnstile 以两种形态出现,它们需要不同的代码。表单中的小组件只是一个两参数调用。而整页插页式挑战则需要从页面抓取两个额外的值,并且只有当你回传识别工具所使用的 user agent 时,token 才会被接受。正是最后这一条要求,导致人们得到那些看起来完美无缺、却每次都失败的 token。

如何区分两者

小组件挑战页面
外观可用表单中的一个复选框整页插页式拦截,页面被阻止
需要 cData 和 chlPageData
需要回传的 user agent

我们的 Turnstile 在线演示 运行的是小组件版本,当你在判断自己面对的是哪一种时,它是一个有用的参考。

设置

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

const solver = new CapSkip({
  host: '127.0.0.1',
  port: 8080,
  recaptchaTimeout: 300,   // seconds, also covers Turnstile
});

CapSkip 在本地识别,因此在任何调用成功之前,桌面应用必须处于运行状态。

小组件模式

const result = await solver.turnstile(
  '0x4AAAAAAA...',                  // the data-sitekey attribute
  'https://example.com/login',
);

console.log(result.code);           // cf-turnstile-response token

result.code 放入 cf-turnstile-response 字段并提交,无需其他任何操作。

挑战页面需要另外两个值

插页式挑战携带与 token 绑定的每次请求状态。其中有两部分必须随识别请求一起传递:

  • cData,作为 data
  • chlPageData,作为 pagedata

这两个值都存在于挑战页面本身,而不是某个表单属性中,因此必须先获取页面才能进行识别。在标准的 Cloudflare 插页式拦截中,它们与 sitekey 一起位于页面自身的挑战选项对象上。它们是一次性的,且与该次页面加载绑定,所以应当获取与识别一并完成,而不要缓存。

const result = await solver.turnstile(sitekey, pageUrl, {
  data: cData,             // the cData value
  pagedata: chlPageData,   // the chlPageData value
  action: 'managed',       // optional, when the page declares one
});

console.log(result.code);
console.log(result.userAgent);   // required for the next step

user agent 不是可选项

Turnstile 将 token 与生成它的浏览器指纹绑定,而 user agent 是该指纹的一部分。CapSkip 会返回它所使用的那个,位于 result.userAgent。如果改用 Node 默认的 fetch user agent 提交 token,Cloudflare 就会拒绝一个本来完全有效的 token。

userAgent 仅在 Turnstile 时才会被填充。对于其他所有验证码类型它都为 undefined,这正是复用现成 reCAPTCHA 辅助代码的人会中招的原因。

const response = await fetch(pageUrl, {
  method: 'POST',
  headers: {
    // Send back the exact user agent the solve was performed with.
    'User-Agent': result.userAgent,
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    'cf-turnstile-response': result.code,
  }),
});

如果 token 被拒绝而 cData 是新鲜的,那几乎肯定就是原因所在。

TypeScript

import { CapSkip, SolveResult } from 'capskip';

const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const result: SolveResult = await solver.turnstile(sitekey, pageUrl);

// userAgent is optional on the type, because only Turnstile populates it.
if (result.userAgent) {
  // safe to forward
}

类型定义随包一起提供,因此无需额外 @types 安装。以下项上的可选类型标注 userAgent 是一个有用的提醒:编译器不会让你忘记其他验证码类型会将其留空。

代理与并发

// Solve through the same egress you will submit from.
await solver.turnstile(sitekey, pageUrl, {
  data: cData,
  pagedata: chlPageData,
  proxy: { type: 'HTTPS', uri: 'user:[email protected]:3128' },
});

// Several at once.
const results = await Promise.all(targets.map((t) =>
  solver.turnstile(t.sitekey, t.url)));

代理适用于 Turnstile、reCAPTCHA 和极验,但不适用于图片验证码,因为图片验证码从不与目标站点交互。对于挑战页面,每次识别都需要各自新获取的 cData,因此应在映射函数内部获取,而不是事先批量获取。

常见问题

小组件需要 cData 吗?

不需要,而且传入空值只会导致识别失败,而不会有任何帮助。只有整页插页式挑战才会用到它们。

token 看起来没问题,但网站却拒绝它。

几乎总是 user agent 的问题。请使用 result.userAgent 提交,而不是使用你的 HTTP 客户端默认发送的那个。第二可能的原因是过期的 cData,它与单次页面加载绑定。

我可以将它与 Puppeteer 一起使用吗?

可以,而且配合得很好:用以下方式从已加载的页面读取 cData page.evaluate,用本 SDK 进行识别,然后在继续之前将相同的 user agent 设置到页面上。我们的 Node.js 验证码识别 页面介绍了自动化方面的内容。

小结

小组件需要一个 sitekey 和一个 URL。挑战页面需要 datapagedata 从页面实时读取,并且 token 必须连同以下内容一起提交: result.userAgent。其余都是普通的 Promise 处理。

其他语言见 Cloudflare Turnstile 识别 页面,参数细节见 API 文档,更全面的 Node 接口见 Node.js 验证码识别 页面。CapSkip 是一款 无限量验证码识别工具 ,它运行在你自己的硬件上。