Explainers

Cloudflare Turnstile 实施检测指南

Cloudflare Turnstile 有三种嵌入方式,检测方法也分三种:静态 HTML 里能读到 data-sitekey,就用正则解析;写在 turnstile.render() 里,就得解析 JS;交互或 XHR 之后才注入 DOM,就必须上 Selenium/Puppeteer 渲染。方法选错,提交给 CaptchaAI 的 sitekey 大概率是空的或过期的。

为什么国内团队更常测 Turnstile

越来越多出海注册页把 reCAPTCHA 换成 Turnstile,对国内 QA 是件好事:

  • reCAPTCHA 脚本挂在 Google 域名下,国内网络访问不稳定,测试常卡在脚本加载这一步。
  • Turnstile 脚本挂在 challenges.cloudflare.com,不依赖 Google 域名,国内加载明显更稳定。

Turnstile 的三种接入方式,先判断用哪种检测

  • HTML 隐式——源码里直接有 <div class="cf-turnstile" data-sitekey="...">,静态 HTML 就能拿到,最简单。
  • JavaScript 显式——脚本里调用 turnstile.render(),需解析 JS,中等难度。
  • 动态加载——用户操作或 XHR 后才注入的小部件,必须执行 JS,难度较高。

方法一:静态 HTML 检测

什么时候能直接匹配

最简单的 Turnstile 集成会直接用 cf-turnstile 类和 data-sitekey 属性,拉一次 HTML 就能看到。

提取代码

import re
import requests

def detect_turnstile_html(url):
    """Detect Turnstile from static HTML."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(url, headers=headers, timeout=15)
    html = response.text

    result = {
        "turnstile_found": False,
        "sitekey": None,
        "mode": None,
        "theme": None,
        "action": None,
        "script_loaded": False,
    }

    # Check for Turnstile script
    if "challenges.cloudflare.com/turnstile" in html:
        result["script_loaded"] = True

    # Check for widget container
    if "cf-turnstile" in html:
        result["turnstile_found"] = True

        # Extract sitekey
        sitekey_match = re.search(
            r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html
        )
        if sitekey_match:
            result["sitekey"] = sitekey_match.group(1)

        # Extract mode
        if 'data-size="invisible"' in html:
            result["mode"] = "invisible"
        elif 'data-appearance="interaction-only"' in html:
            result["mode"] = "non-interactive"
        else:
            result["mode"] = "managed"

        # Extract theme
        theme_match = re.search(r'data-theme=["\'](\w+)["\']', html)
        if theme_match:
            result["theme"] = theme_match.group(1)

        # Extract action
        action_match = re.search(r'data-action=["\']([^"\']+)["\']', html)
        if action_match:
            result["action"] = action_match.group(1)

    return result


# Usage
info = detect_turnstile_html("https://staging.example.com/qa-login")
if info["turnstile_found"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")

方法二:JavaScript API 检测

什么时候要解析 JS

有些站点不写 HTML 属性,而是在脚本里调用 turnstile.render(),静态正则匹配不到容器,得从脚本文本里抠配置对象。

检测代码

import re

def detect_turnstile_js_api(html):
    """Detect Turnstile from JavaScript render calls."""
    patterns = [
        # turnstile.render('#element', {sitekey: '...'})
        r"turnstile\.render\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*\{([^}]+)\}",
        # turnstile.render(element, {sitekey: '...'})
        r"turnstile\.render\s*\([^,]+,\s*\{([^}]+)\}",
    ]

    for pattern in patterns:
        match = re.search(pattern, html, re.DOTALL)
        if match:
            config_text = match.group(match.lastindex)

            # Extract sitekey from config object
            sitekey_match = re.search(
                r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", config_text
            )
            # Extract callback
            callback_match = re.search(
                r"callback\s*:\s*(\w+|function)", config_text
            )
            # Extract action
            action_match = re.search(
                r"action\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )
            # Extract appearance
            appearance_match = re.search(
                r"appearance\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )

            return {
                "found": True,
                "method": "javascript_api",
                "sitekey": sitekey_match.group(1) if sitekey_match else None,
                "callback": callback_match.group(1) if callback_match else None,
                "action": action_match.group(1) if action_match else None,
                "appearance": appearance_match.group(1) if appearance_match else None,
            }

    return {"found": False, "method": None}

方法三:动态加载检测(Selenium/Puppeteer)

  • Turnstile 在交互或 XHR 后才注入页面时,只能上浏览器自动化。
  • Python(Selenium)和 Node.js(Puppeteer)逻辑一致:等页面加载完成,查 iframe、容器和隐藏字段。

Python(Selenium)

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import re

def detect_turnstile_dynamic(url):
    """Detect dynamically loaded Turnstile using Selenium."""
    options = webdriver.ChromeOptions()
    options.add_argument("--no-sandbox")
    driver = webdriver.Chrome(options=options)

    try:
        driver.get(url)

        # Wait for page to fully load
        WebDriverWait(driver, 10).until(
            lambda d: d.execute_script("return document.readyState") == "complete"
        )

        result = {
            "turnstile_found": False,
            "sitekey": None,
            "iframe_present": False,
            "response_field": False,
        }

        # Check for Turnstile iframe
        iframes = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='challenges.cloudflare.com']")
        if iframes:
            result["turnstile_found"] = True
            result["iframe_present"] = True

        # Check for cf-turnstile container
        containers = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
        for container in containers:
            sitekey = container.get_attribute("data-sitekey")
            if sitekey:
                result["turnstile_found"] = True
                result["sitekey"] = sitekey

        # Check for hidden response field
        response_fields = driver.find_elements(
            By.CSS_SELECTOR, "[name='cf-turnstile-response'], [name='g-recaptcha-response']"
        )
        if response_fields:
            result["response_field"] = True

        # Check page source for JS API render
        page_source = driver.page_source
        js_match = re.search(
            r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", page_source
        )
        if js_match and not result["sitekey"]:
            result["sitekey"] = js_match.group(1)
            result["turnstile_found"] = True

        return result

    finally:
        driver.quit()

Node.js(Puppeteer)

const puppeteer = require("puppeteer");

async function detectTurnstileDynamic(url) {
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--no-sandbox"],
  });

  const page = await browser.newPage();

  const result = {
    turnstileFound: false,
    sitekey: null,
    iframePresent: false,
    responseField: false,
    scriptUrl: null,
  };

  // Monitor network for Turnstile script
  page.on("response", (response) => {
    if (response.url().includes("challenges.cloudflare.com/turnstile")) {
      result.scriptUrl = response.url();
    }
  });

  await page.goto(url, { waitUntil: "networkidle2" });

  // Check for Turnstile container
  const sitekey = await page.evaluate(() => {
    const el = document.querySelector(
      ".cf-turnstile, [data-sitekey]"
    );
    return el ? el.getAttribute("data-sitekey") : null;
  });

  if (sitekey) {
    result.turnstileFound = true;
    result.sitekey = sitekey;
  }

  // Check for Turnstile iframe
  const iframes = await page.$$("iframe[src*='challenges.cloudflare.com']");
  if (iframes.length > 0) {
    result.turnstileFound = true;
    result.iframePresent = true;
  }

  // Check for response field
  const responseField = await page.$(
    "[name='cf-turnstile-response']"
  );
  result.responseField = !!responseField;

  await browser.close();
  return result;
}

detectTurnstileDynamic("https://staging.example.com/qa-login").then(console.log);

四合一:完整检测类 TurnstileDetector

  • 合并三种方法,传一个 URL 进去即可。
  • 依次检查脚本标签、cf-turnstile 容器、data-sitekeyturnstile.render() 配置,一次返回完整结果。
import re
import requests

class TurnstileDetector:
    """Detect Cloudflare Turnstile across all implementation methods."""

    TURNSTILE_SCRIPT = "challenges.cloudflare.com/turnstile"
    SITEKEY_PATTERNS = [
        r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
        r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"TURNSTILE_SITE_KEY\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
    ]

    def __init__(self, url, html=None):
        self.url = url
        self.html = html
        if not self.html:
            self._fetch()

    def _fetch(self):
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/120.0.0.0",
            "Accept": "text/html,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        }
        response = requests.get(self.url, headers=headers, timeout=15)
        self.html = response.text

    def detect(self):
        """Run all detection methods and return results."""
        return {
            "url": self.url,
            "turnstile_present": self.has_turnstile(),
            "sitekey": self.extract_sitekey(),
            "mode": self.detect_mode(),
            "implementation": self.detect_implementation(),
            "script_loaded": self.has_script(),
            "response_field": self.has_response_field(),
            "action": self.extract_action(),
            "theme": self.extract_theme(),
        }

    def has_turnstile(self):
        return (
            self.has_script()
            or "cf-turnstile" in self.html
            or self.extract_sitekey() is not None
        )

    def has_script(self):
        return self.TURNSTILE_SCRIPT in self.html

    def has_response_field(self):
        return "cf-turnstile-response" in self.html

    def extract_sitekey(self):
        for pattern in self.SITEKEY_PATTERNS:
            match = re.search(pattern, self.html)
            if match:
                return match.group(1)
        return None

    def detect_mode(self):
        if 'data-size="invisible"' in self.html or "size: 'invisible'" in self.html:
            return "invisible"
        if 'data-appearance="interaction-only"' in self.html:
            return "non-interactive"
        if "cf-turnstile" in self.html:
            return "managed"
        return "unknown"

    def detect_implementation(self):
        if "cf-turnstile" in self.html and re.search(r"data-sitekey=", self.html):
            return "html_implicit"
        if "turnstile.render" in self.html:
            return "javascript_explicit"
        if self.has_script() and not "cf-turnstile" in self.html:
            return "dynamic_loading"
        return "unknown"

    def extract_action(self):
        match = re.search(r'data-action=["\']([^"\']+)["\']', self.html)
        if match:
            return match.group(1)
        match = re.search(r"action\s*:\s*['\"]([^'\"]+)['\"]", self.html)
        return match.group(1) if match else None

    def extract_theme(self):
        match = re.search(r'data-theme=["\'](\w+)["\']', self.html)
        return match.group(1) if match else "auto"


# Usage
detector = TurnstileDetector("https://staging.example.com/qa-login")
info = detector.detect()

if info["turnstile_present"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")
    print(f"Implementation: {info['implementation']}")

检测到 sitekey 后,怎么提交给 CaptchaAI

  1. 把 sitekey、页面 URL(以及页面上的 data-action)组装成参数,POST 到 in.php
  2. 轮询 res.phpstatus == 1 即为识别完成的 token。
import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_detected_turnstile(detection_result):
    """Solve Turnstile using detection results."""
    if not detection_result["turnstile_present"]:
        raise ValueError("No Turnstile detected")

    if not detection_result["sitekey"]:
        raise ValueError("Sitekey not found — may need browser-based extraction")

    params = {
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": detection_result["sitekey"],
        "pageurl": detection_result["url"],
        "json": 1,
    }

    # Include action if present
    if detection_result.get("action"):
        params["action"] = detection_result["action"]

    submit = requests.post("https://ocr.captchaai.com/in.php", data=params)
    task_id = submit.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Turnstile solve timed out")


# Full workflow
detector = TurnstileDetector("https://example.com/signup")
info = detector.detect()

if info["turnstile_present"]:
    token = solve_detected_turnstile(info)
    print(f"Token: {token[:50]}...")

特殊场景速查

  • sitekey 在外部 JS 文件里——拉取关联 JS 文件,同样正则扫一遍。
  • sitekey 来自接口返回值——监控请求,从 JSON 响应里找。
  • 同一页面多个 Turnstile 组件——sitekey 和对应表单关联,别只取第一个。
  • Turnstile 在 shadow DOM 里——浏览器上下文里用 shadowRoot.querySelector
  • sitekey 藏在内联 <script>——检查所有 <script> 标签的配置对象。
  • 登录之后才出现——先完成认证,再进入检测。

实操经验:先试静态 HTML 解析,命中率很高;确认拿不到 cf-turnstile 容器再上 Selenium/Puppeteer,能省不少调试成本。


故障排除对照表

现象 原因 处理方式
有脚本标签,无 sitekey JS API 配置来自别的文件 检查所有关联 JS 文件和 XHR 响应
sitekey 提取错误 页面有多个验证码组件 把 sitekey 与周围表单元素对应
检测正常但求解失败 服务端校验 action 参数 请求里带上 data-action
初始 HTML 无该组件 交互后才动态加载 用 Selenium/Puppeteer 完整渲染
cf-turnstile-response 为空 widget 未加载完成 等待 widget 加载完成

常见问题

Turnstile 的检测思路和 reCAPTCHA 一样吗?

不完全一样。reCAPTCHA 用 g-recaptcha-response 字段,Turnstile 用 cf-turnstile 容器和 data-sitekey,脚本域名也不同。

sitekey 会不会随着页面改版而变化?

会。站点可随时轮换 sitekey,别硬编码,每次任务前重新提取。

只用 requests 库能检测到动态加载的 Turnstile 吗?

大多数情况不能。交互或 XHR 后才注入 DOM 的话,静态请求拿到的 HTML 里没有 cf-turnstile 容器,得用 Selenium/Puppeteer 渲染后再检测。

什么情况下必须带上 data-action 参数?

只有服务端校验时才需要。HTML 里出现了 data-action,原样传给解决请求即可。

同一页面出现多个 Turnstile 组件,检测结果会混乱吗?

有风险。要把每个 cf-turnstile 容器和对应表单关联,分别提交各自的 sitekey,不能只取第一个。

检测到的 sitekey 需要缓存吗?

不建议。sitekey 会随页面改版轮换,每次任务前重新提取更稳妥。


小结

  • 按顺序检查脚本标签、cf-turnstile 容器、data-sitekeyturnstile.render(),就能定位实现方式;简单集成用静态解析,动态加载才需要浏览器自动化。
  • 检测到 sitekey 后,用它和页面 URL 提交给 CaptchaAI 的 Turnstile 求解器,各接入方式的提交流程都一样。

相关文章

该文章已禁用评论。