Tutorials

使用 Node.js 和 CaptchaAI 解决 Cloudflare Turnstile 问题

Node.js 里处理 Cloudflare Turnstile 只有三个动作:拿到 0x 开头的 sitekey,用 method=turnstile 提交给 CaptchaAI,把返回的 token 以 cf-turnstile-response 发回表单。Node.js 18+ 自带 fetch,全程不装额外依赖。

国内站点更常见的是极验滑块,但做出海业务或跨境电商自建后台,就会频繁撞上 Turnstile。下面的代码在自有 staging 环境里跑通,可直接抄用。

环境准备

  • Node.js 18+:fetchURLSearchParams 已内置。
  • 一个 CaptchaAI API Key,放进环境变量,别硬编码进仓库。
  • 国内机器建议先配好 npm 镜像:npm config set registry https://registry.npmmirror.com

第 1 步:从页面提取 Turnstile sitekey

Turnstile 的 sitekey 以 0x 开头,reCAPTCHA 以 6Le 开头,看首字符就能分清。它通常藏在四个位置:cf-turnstile 容器上、其他元素的 data-sitekeyturnstile.render() 参数,或内联脚本里。下面按命中率依次匹配:

async function extractTurnstileSitekey(url) {
  const resp = await fetch(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    },
  });
  const html = await resp.text();

  // Method 1: data-sitekey attribute on Turnstile div
  const divMatch = html.match(
    /class=["'][^"]*cf-turnstile[^"]*["'][^>]*data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/
  );
  if (divMatch) return divMatch[1];

  // Method 2: data-sitekey on any element (Turnstile keys start with 0x)
  const attrMatch = html.match(
    /data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/
  );
  if (attrMatch) return attrMatch[1];

  // Method 3: In JavaScript turnstile.render call
  const jsMatch = html.match(
    /turnstile\.render\s*\([^,]+,\s*\{[^}]*sitekey\s*:\s*["']([0-9x][A-Za-z0-9_-]+)["']/
  );
  if (jsMatch) return jsMatch[1];

  // Method 4: Generic sitekey in inline script
  const inlineMatch = html.match(
    /sitekey\s*:\s*["'](0x[A-Za-z0-9_-]+)["']/
  );
  if (inlineMatch) return inlineMatch[1];

  return null;
}

第 2 步:调用 CaptchaAI 识别 Turnstile

四种匹配都为空时,先打印 html 看看是不是被整页 challenge 挡住了——那属于 Cloudflare Challenge,处理方式不同。

提交任务用 in.php,取结果用 res.php,都带 json=1。Turnstile 的识别耗时通常在 10 秒以内,30 次、间隔 5 秒的轮询上限很宽松:

const API_KEY = "YOUR_API_KEY";

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveTurnstile(sitekey, pageurl, action = null) {
  // Submit task
  const submitData = {
    key: API_KEY,
    method: "turnstile",
    sitekey: sitekey,
    pageurl: pageurl,
    json: "1",
  };

  if (action) {
    submitData.action = action;
  }

  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams(submitData),
  });
  const submitResult = await submitResp.json();

  if (submitResult.status !== 1) {
    throw new Error(`Submit error: ${submitResult.request}`);
  }

  const taskId = submitResult.request;
  console.log(`Task ID: ${taskId}`);

  // Poll for result
  for (let i = 0; i < 30; i++) {
    await sleep(5000);

    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const pollResult = await pollResp.json();

    if (pollResult.status === 1) {
      return pollResult.request;
    }

    if (pollResult.request === "ERROR_CAPTCHA_UNSOLVABLE") {
      throw new Error("Turnstile unsolvable");
    }
  }

  throw new Error("Solve timed out");
}

第 3 步:把 token 提交回表单

注意两点:pageurl 必须是验证码实际出现的页面地址;收到 ERROR_CAPTCHA_UNSOLVABLE 就换新任务重试,别在同一个 ID 上死等。

token 就是一个字符串,塞进 cf-turnstile-response 和其余字段一起 POST 回去。User-Agent 要与抓页面时一致,这是 403 最常见的来源:

async function submitTurnstileForm(url, formData, token) {
  const body = new URLSearchParams({
    ...formData,
    "cf-turnstile-response": token,
  });

  const resp = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    },
    body,
  });

  return {
    status: resp.status,
    body: await resp.text(),
  };
}

串起来:一次完整的登录流程

三个函数接起来就是一个最小闭环。示例地址指向自建的 staging 站点,先在测试环境跑通再接业务:

async function loginWithTurnstile(loginUrl, credentials) {
  // Step 1: Extract sitekey
  const sitekey = await extractTurnstileSitekey(loginUrl);
  if (!sitekey) {
    throw new Error("Turnstile sitekey not found");
  }
  console.log(`Sitekey: ${sitekey}`);

  // Step 2: Solve Turnstile
  const token = await solveTurnstile(sitekey, loginUrl);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Step 3: Submit form
  const result = await submitTurnstileForm(loginUrl, credentials, token);
  console.log(`Result: ${result.status}`);

  return result;
}

// Usage
const result = await loginWithTurnstile("https://staging.example.com/qa-login", {
  email: "[email protected]",
  password: "pass123",
});

生产环境封装:TurnstileSolver 类

脚本跑通后,把检测、提交、轮询收进一个类,API Key 用私有字段保存,后续换队列驱动改动更小:

class TurnstileSolver {
  #apiKey;

  constructor(apiKey) {
    this.#apiKey = apiKey;
  }

  async solve(sitekey, pageurl, options = {}) {
    const taskId = await this.#submit(sitekey, pageurl, options);
    return await this.#poll(taskId);
  }

  async detectAndSolve(url) {
    const sitekey = await this.#detect(url);
    if (!sitekey) throw new Error("No Turnstile found");
    return await this.solve(sitekey, url);
  }

  async #detect(url) {
    const resp = await fetch(url, {
      headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" },
    });
    const html = await resp.text();
    const match = html.match(/data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/);
    return match ? match[1] : null;
  }

  async #submit(sitekey, pageurl, options) {
    const body = new URLSearchParams({
      key: this.#apiKey,
      method: "turnstile",
      sitekey,
      pageurl,
      json: "1",
      ...(options.action && { action: options.action }),
      ...(options.cdata && { data: options.cdata }),
    });

    const resp = await fetch("https://ocr.captchaai.com/in.php", {
      method: "POST",
      body,
    });
    const data = await resp.json();

    if (data.status !== 1) throw new Error(`Submit: ${data.request}`);
    return data.request;
  }

  async #poll(taskId) {
    const params = new URLSearchParams({
      key: this.#apiKey,
      action: "get",
      id: taskId,
      json: "1",
    });

    for (let i = 0; i < 30; i++) {
      await new Promise((r) => setTimeout(r, 5000));
      const resp = await fetch(`https://ocr.captchaai.com/res.php?${params}`);
      const data = await resp.json();

      if (data.status === 1) return data.request;
      if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") {
        throw new Error("Unsolvable");
      }
    }
    throw new Error("Timed out");
  }
}

// Usage
const solver = new TurnstileSolver("YOUR_API_KEY");
const token = await solver.detectAndSolve("https://staging.example.com/qa-login");

处理 action 与 cData 参数

有些站点渲染 Turnstile 时会带上 actioncData。它们参与 token 校验,漏传就判失败,且报错笼统难定位。先从 HTML 取出 data-action,提交时一并传给 CaptchaAI,cData 对应请求里的 data

// Extract action from the page
function extractTurnstileAction(html) {
  const match = html.match(
    /data-action=["']([^"']+)["']|action\s*:\s*["']([^"']+)["']/
  );
  return match ? match[1] || match[2] : null;
}

// Solve with action
const token = await solver.solve(sitekey, pageurl, {
  action: "login",
  cdata: "session_abc123",
});

在自己的服务端校验 Turnstile token

如果接入方服务端也归你负责,校验请求发给 Cloudflare 的 siteverify 接口,密钥用站点后台的 secret key,可确认 token 是否有效:

async function verifyTurnstileToken(token, ip) {
  const resp = await fetch(
    "https://challenges.cloudflare.com/turnstile/v0/siteverify",
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        secret: "YOUR_TURNSTILE_SECRET_KEY",
        response: token,
        remoteip: ip,
      }),
    }
  );

  const data = await resp.json();
  return data.success;
}

常见报错与排查

现象 原因 处理方式
sitekey 以 6Le 开头 那是 reCAPTCHA 改用 method=userrecaptcha
token 被拒绝 sitekey 取错或放置过久 重取 sitekey,拿到即提交
抓不到 sitekey 组件由 JavaScript 渲染 用 Puppeteer 或 Playwright 读 DOM
ERROR_BAD_PARAMETERS 缺 sitekey 或 pageurl 检查两个参数
提交后返回 403 请求头前后不一致 两次请求用同一套 User-Agent

并发怎么算,套餐怎么选

CaptchaAI 按线程计费,套餐内识别次数不限量,吞吐量按“线程数 ÷ 单次耗时”估算,价格按美元计价:

  • BASIC($15/月,5 线程):单机脚本、日常 QA 回归。
  • STANDARD($30/月,15 线程):多任务并行。
  • ADVANCE($90/月,50 线程):定时批量作业、多站点巡检。

把固定 5 秒轮询改成前三次 3 秒、之后退避到 5 秒,平均等待和线程占用都会下降。


常见问题

token 拿到后必须马上提交吗?

是的。Turnstile token 一次性、有效期短,拿到后应立刻提交表单,不缓存也不复用。写库、发通知等动作排到提交之后。

页面里抓不到 sitekey 怎么办?

说明组件是脚本运行时插入的,静态 HTML 里自然没有。改用 Puppeteer 或 Playwright 打开页面,等 .cf-turnstile 出现后再读 data-sitekey,后面的流程不用改。

CaptchaAI 能识别 hCaptcha 或 GeeTest v4 吗?

不支持 hCaptcha,也不支持 FunCaptcha(Arkose Labs);GeeTest v4 属于即将支持的类型。当前可用的有 reCAPTCHA v2/v3 全系、Cloudflare Turnstile 与 Challenge、GeeTest v3、图片与九宫格验证码、BLS,以及 CaptchaFox、Friendly Captcha、Lemin 三种测试版类型。

国内网络环境下调试要注意什么?

Turnstile 的脚本和校验接口都在 challenges.cloudflare.com,不同出口网络延迟差别很大,本地跑不通未必是代码问题,放到与线上一致的服务器上更可靠。采集类脚本请遵守 robots 协议与个人信息保护法等要求。


小结

关键只有三个字段:0x 开头的 sitekey、method=turnstilecf-turnstile-response。识别交给 CaptchaAI,你的代码只剩抓取、提交和重试。

相关文章

该文章已禁用评论。