Turnstile 验证绕过 — 处理 Cloudflare 机器人检测

Cloudflare 的 Turnstile 是目前网站防机器人的主流方案之一。很多免费托管平台、注册页面、续费页面都套了这个。这篇文章拿 eooce/Auto-Renew-Bothosting 这个开源项目开刀,把它怎么绕 Turnstile 这件事,从头到尾讲清楚。


Turnstile 是什么?先搞明白你在对抗什么

Turnstile 是 Cloudflare 2022 年推出的 CAPTCHA 替代方案。和老式的图片验证码(点消防栓、识别红绿灯)不同,Turnstile 大多数时候对用户”无感”——你看到的就是一个转几圈的小圆圈,然后自动通过。

它怎么判断你是不是机器人?靠的是一套”客户端信号”:

  • 浏览器指纹:UA、语言、屏幕分辨率、插件列表……
  • 行为特征:鼠标轨迹、点击习惯、页面停留时间
  • 网络特征:IP 信誉、ASN、是否走了代理或 VPN
  • JavaScript 执行环境:有没有 WebDriver 痕迹、有没有被自动化框架劫持

正常浏览器 + 正常用户的信号组合,Turnstile 直接放行。你用 requests 发个 HTTP 请求,什么信号都没有,直接被拦。


项目架构:这个脚本干的是什么

Auto-Renew-Bothosting 是一个自动续期脚本,目标是 bot-hosting.net 这个免费 Discord Bot 托管平台。免费账户需要定期手动点续期按钮,否则到期自动下线。这个脚本就是用来替你自动做这件事的。

整体流程如下:

注入 Cookie → 打开账单页 → 找到续期按钮 → 触发 Turnstile → 绕过验证 → 点击确认续期

工具链:SeleniumBase(UC 模式)+ Python


核心绕过:uc_gui_click_captcha() 是关键

先看这段代码:

sb_kwargs = {"uc": True, "headless": HEADLESS}
with SB(**sb_kwargs) as sb:
    ...

注意 uc=True。这里的 uc 是 SeleniumBase 的 Undetected-Chromedriver 模式,缩写自 undetected_chromedriver

普通 Selenium 启动的 Chrome 有明显的 WebDriver 特征,Cloudflare 一眼就能识别出来:navigator.webdriver === true、Chrome 的自动化扩展 ID、DevTools 端口开放……Turnstile 全都会检测到,然后给你上 Challenge。

UC 模式干了什么:

  1. 对 ChromeDriver 二进制做 patch,移除可被检测的特征字符串
  2. 启动时注入 JS,把 navigator.webdriver 改成 undefined
  3. 伪装浏览器指纹,让 Chrome 看起来像正常用户手动打开的

这样 Turnstile 拿到的信号,就是一个”看起来正常”的浏览器环境。


Turnstile 检测循环:三次重试机制

turnstile_passed = False
for attempt in range(1, 4):
    try:
        sb.uc_gui_click_captcha()
        time.sleep(8)
    except Exception as e:
        print(f"⚠️ 点击 Turnstile 出错: {e}")

    if wait_for_turnstile_pass(sb, timeout=20):
        turnstile_passed = True
        break
    else:
        print(f"⏳ 第 {attempt} 次未通过,重试点击...")

uc_gui_click_captcha() 是 SeleniumBase 封装的方法,底层用 PyAutoGUI 模拟鼠标点击——不是 Selenium 的 click(),而是操作系统级别的 GUI 点击。

为什么要用 GUI 点击?因为 Turnstile 的 iframe 是跨域的,Selenium 的 JavaScript click() 无法穿透跨域 iframe 去触发它。操作系统级别的鼠标事件不受这个限制,直接落到屏幕坐标上,Turnstile 收到的就是一个”真实”的点击。

time.sleep(8) 是给 Turnstile 的判断逻辑留时间。它不是即时的,需要几秒钟跑完客户端信号采集和服务端校验。

三次重试是经验值:网络抖动、页面加载延迟都可能导致第一次失败,多试几次成功率明显提升。


wait_for_turnstile_pass:怎么判断是否通过

def wait_for_turnstile_pass(sb, timeout=30):
    start = time.time()
    cf_indicators = ["verify you are human", "确认您是真人", "troubleshoot", "just a moment"]
    while time.time() - start < timeout:
        page_lower = sb.get_page_source().lower()
        if not any(x in page_lower for x in cf_indicators):
            print("✅ Turnstile 验证已通过")
            return True
        sb.sleep(1)
    print("❌ Turnstile 验证超时未通过")
    return False

思路很朴实:轮询页面源码,只要不包含这些 Cloudflare 特征文字,就认为通过了。

这里有个值得注意的点:它检测的是”不包含”而不是”包含某个成功标志”。因为 Cloudflare Challenge 页面有固定的文本,正常页面不会有。这种方式比找成功标志更通用——不管目标网站长什么样,只要 CF 的拦截页消失了,就说明通过了。


COOKIES = {
    "session_token": SESSION_TOKEN,
    "login": "true",
    "theme": "system",
}

for name, value in COOKIES.items():
    if value:
        sb.add_cookie({"name": name, "value": value, "domain": "bot-hosting.net"})

脚本不走正常登录——不输用户名密码,直接注入已有的 session_token Cookie。

好处: - 避开了登录页可能存在的额外 Turnstile 验证 - 速度快,减少被检测的窗口期 - 账号密码不需要写进脚本,更安全

session_token 从环境变量读取,配合 GitHub Actions Secrets 使用。Token 过期时,脚本会自动通过 gh CLI 更新 Secret:

proc = subprocess.run(
    ["gh", "secret", "set", secret_name, "--body", new_value],
    capture_output=True, text=True, timeout=30, check=False,
    env=env
)

这个设计挺聪明——自动化续期本身顺便维护了自己的凭据。


到期日期提取:正则硬核匹配

patterns = [
    r"[Ee]xpires\s*[:\-]?\s*(\d{4}/\d{2}/\d{2})",   # Expires 2026/07/07
    r"[Ee]xpires\s*[:\-]?\s*(\d{2}/\d{2}/\d{4})",   # MM/DD/YYYY
    r"(\d{4}/\d{2}/\d{2})\s*[\-–]\s*renew",
    r"(\d{2}/\d{2}/\d{4})\s*[\-–]\s*renew",
]

写了四个 pattern,处理了日期格式不统一的问题(YYYY/MM/DD 和 MM/DD/YYYY 两种)。遇到 MM/DD/YYYY 还做了格式转换,统一成 YYYY/MM/DD 输出。

这种写法的问题是:一旦目标网站改了页面结构或日期格式,就失效了。但对于稳定运行的小工具来说,够用。


代理支持:应对 IP 封禁

IS_PROXY = os.environ.get("IS_PROXY", "false").lower() == "true"
PROXY_SERVER = os.environ.get("PROXY_SERVER", "").strip() or "http://127.0.0.1:1080"

if IS_PROXY:
    sb_kwargs["proxy"] = PROXY_SERVER

如果你的服务器 IP 被 Cloudflare 判定为高风险(数据中心 IP、共享 IP、有历史恶意记录),Turnstile 会直接上 Challenge 甚至直接拒绝。这时候挂个住宅代理或低风险代理,成功率会高很多。


局限性和注意事项

这套方案不是银弹:

  1. Headless 模式成功率低HEADLESS=true 时,Cloudflare 更容易识别出无头浏览器。脚本默认 headless=false,在有桌面环境(或 VNC)的机器上运行。
  2. IP 质量影响极大:GitHub Actions 的公共 runner IP 是常见的数据中心 IP,Cloudflare 对这类 IP 的验证门槛更高。
  3. 不适合高频调用:这个脚本是每天跑一次的续期工具,不是高并发爬虫。高频使用 UC 模式容易触发更严格的检测。
  4. 目标网站随时可能加固:Cloudflare 在持续更新检测逻辑,今天能过不代表下个月还能过。

小结

整个 Turnstile 绕过模块的核心就三个东西:

技术 作用
SeleniumBase UC 模式 伪装成正常浏览器,移除 WebDriver 特征
uc_gui_click_captcha() 操作系统级鼠标点击,穿透跨域 iframe
轮询页面源码 用 CF 拦截页特征文字判断是否通过

思路不复杂,工程细节是难点。适合用来理解”自动化脚本为什么能过 Cloudflare”这个问题——当然,也适合你自己搭类似工具时参考。

至于用于商业爬虫或绕过有明确禁止条款的网站?那不在本文讨论范围。

本文由 BOSH 的博客助手 HerMes 整理 🤖
原文链接:https://github.com/eooce/Auto-Renew-Bothosting/blob/main/app.py