vibecoding.site
← back to blog
约 9 分钟3 标签

一天搞定 Cloudflare Turnstile:用 AI 协作改写一个签到脚本

某个国外站点把验证码升级到 Turnstile,旧自动化脚本失效。三轮 vibe-coding 完成对接的过程与教训。


title: 一天搞定 Cloudflare Turnstile:用 AI 协作改写一个签到脚本 slug: cloudflare-turnstile-rewrite publishedAt: 2026-04-28 excerpt: 某个国外站点把验证码升级到 Turnstile,旧自动化脚本失效。三轮 vibe-coding 完成对接的过程与教训。 tags: [automation, case-study, turnstile] readingMin: 9 wordCount: 1430 draft: false

朋友委托:旧签到脚本挂了

某个国外站点把验证码从老式 reCAPTCHA v2 升级到了 Cloudflare Turnstile。朋友手里有一个跑了一年多的 Python 签到脚本,升级当天直接 401。来找我帮忙复活。

事情不复杂,但有几个看点:

  • 从未碰过 Turnstile:训练数据里 Turnstile 文档不算多,模型回答经常飘
  • 不能让朋友停机太久:他订阅了 VIP,断签到就掉等级
  • 不能签到失败之后被站点拉黑:Turnstile 失败次数过多会进黑名单,要小心试

整个事情用 3 轮 vibe-coding 跑通,这里把过程写一遍。所有具体的站点信息全部模糊化 —— 这不是教程,是案例研究

第一轮:理解 Turnstile 到底是什么

让 AI 干活之前先让自己懂。读了 Cloudflare 官方文档,几条关键事实:

  • Turnstile 是无感知验证码:不像 reCAPTCHA 让用户选图,Turnstile 在后台跑一组 fingerprint 检测,绝大多数情况用户什么都不用做
  • 页面前端会有一个 <div class="cf-turnstile" data-sitekey="0xAAAA...">,Turnstile 的 JS 在这个 div 里渲染一个隐藏 iframe
  • 验证通过后,iframe 回写一个 cf-turnstile-response 隐藏 <input>,里面是一段 token
  • 后端要做的事:把 token 提交给 /api/.../action,站点服务端用自己的 secret key 去 Cloudflare 校验 token

所以自动化脚本要做的不是"破解 Turnstile",而是让 Turnstile 像对真实浏览器一样,正常完成 fingerprint 检测,然后拿到 token 提交

区分两种"绕过 Turnstile"的需求

这一点必须说清楚:

  • 正当:你是这个站点的合法用户,你有账号,你想自动化日常操作 —— 这是脚本应该解决的
  • 不正当:你想批量注册 / 刷接口 / 攻击站点 —— 这是 Turnstile 设计要挡的

朋友的需求毫无疑问是前者:已有 VIP 账号,只是想自动签到。但工程实现上两种需求看起来一样,所以脚本写得越保守越好 —— 绝不批量调用,签到完立即退出,日内只跑一次

第二轮:让 AI 写第一稿

把背景丢给 Claude:

用户有一个 Python 签到脚本,目标站点验证码从 reCAPTCHA 升级到了 Cloudflare Turnstile。脚本不需要破解 Turnstile,需要的是真实浏览器自动化让 Turnstile 跑过,拿到 token 提交。请给一个 Playwright 方案。

Claude 给的第一稿大致这样:

from playwright.sync_api import sync_playwright
 
def sign_in(username: str, password: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        ctx = browser.new_context()
        page = ctx.new_page()
 
        page.goto("https://example.com/login")
        page.fill('input[name="username"]', username)
        page.fill('input[name="password"]', password)
 
        # 等 Turnstile 完成
        page.wait_for_selector('input[name="cf-turnstile-response"]', state="attached")
        token = page.input_value('input[name="cf-turnstile-response"]')
 
        page.click('button[type="submit"]')
        page.wait_for_url("**/dashboard")
        browser.close()

看起来合理,但跑起来 80% 失败

第一稿失败的两个原因

观察了几次,失败模式是:

  1. headless=True 时 Turnstile 直接拒绝:Cloudflare 能识别 headless Chromium 的特征(navigator.webdriver / 缺少 plugins / 等等),直接判定为 bot
  2. input 字段填充太快:page.fill 是瞬间填的,真实用户敲键盘有时序。Turnstile 的 fingerprint 检测会看 input event 的时间分布

修法分别是:

browser = p.chromium.launch(
    headless=False,  # 关键:用有头模式
    args=[
        "--disable-blink-features=AutomationControlled",  # 隐藏 webdriver 标志
    ],
)
 
# 用 type 而非 fill,模拟真实敲键
page.type('input[name="username"]', username, delay=80)
page.type('input[name="password"]', password, delay=80)

delay=80 是关键:每个按键间隔 ~80ms,接近真实人类敲键速度(每分钟 ~150 字符)。这条改完后通过率从 20% 拉到 ~85%。

第三轮:让脚本"诚实"

跑了几天 85% 通过率,剩 15% 失败有几种:

  • 服务器在欧洲机房,触发了一些地理风控
  • Cloudflare 偶尔给"挑战"模式:不是无感验证,而是要 click 一个复选框
  • 签到接口本身有 rate limit:同一 IP 一天调 3 次以上会 429

最终的脚本架构是这样的:

def sign_in_with_retry(max_retries: int = 2) -> bool:
    """每天只签到一次,失败最多重试 2 次,中间间隔 5 分钟。"""
    for attempt in range(max_retries + 1):
        try:
            result = sign_in()
            if result == "success":
                return True
            if result == "challenged":
                # Turnstile 给挑战 → 人工接入
                notify_owner("turnstile_challenged")
                return False
            if result == "rate_limited":
                # 站点 429 → 直接放弃当天
                return False
        except Exception as e:
            log_error(e)
        if attempt < max_retries:
            time.sleep(300)  # 5 分钟后重试
    return False

几条工程纪律值得记下来:

  1. 失败有分类:不是所有失败都该重试。challenged / rate_limited 这种本质上不可重试的,要识别后直接退出
  2. 跑前再核对最后签到时间:防止脚本重启被两次执行
  3. 失败通知机制:用最简陋的方式(Telegram bot / 邮件),让脚本"知道"有人会管它
  4. 绝不并发:一个账号一次只跑一个 instance

长期跑下来的稳定性

部署到 cron(每天 UTC 02:00 跑一次)之后,大约 95% 一次成功,4% 重试一次成功,1% 完全失败需要人工。考虑到 Turnstile 的设计是就是为了让自动化变难,这个数字已经足够好。

AI 协作在这种案例里能干什么

事后复盘,这一天的工作里 AI 真正帮上忙的是:

  • 第一轮的"读文档 + 输出结构化总结":让我快速理解 Turnstile 是什么、要做什么
  • 第二轮的"写脚本第一稿":省了我手敲 Playwright API 的时间
  • debug 失败模式时的"假设 → 验证"循环:我看 log,AI 提假设,我去试,reciprocate 几轮就找到问题

AI 能帮上的是:

  • 不能告诉我 headless 会被 Cloudflare 识别:这条要真跑了一遍才能观察到。AI 训练数据里没有这条具体的失败模式
  • 不能告诉我 delay=80ms 这个数字:这是基于"接近人类敲键速度"经验值,AI 一开始给的是 delay=50,跑下来发现还是太快

所以"协作"的本质,是 AI 提供"广度",我提供"深度":AI 让我快速贴近问题,真正 debug 时仍需要人在 loop。

给在做类似事情的人的建议

最后落几条非泛泛而谈的:

  1. headless=False 是 Turnstile 自动化的起点,不是终点,但起点必须正确
  2. type(..., delay=80) 而不是 fill(...),这一条变化能从 20% 拉到 85%
  3. 写一个"挑战模式被触发"的人工接入路径:不是所有失败都该自动重试
  4. 签到前后都打 log:出问题时你需要知道是 Turnstile 没过 / 登录失败 / 还是 rate limit

这篇没有放完整代码,因为完整代码会暴露目标站点信息,违反了"对朋友项目保密"的承诺。这是一个案例研究,不是教程。

评论

加载评论中…

留下评论 · 提交后由 PM 审核显示

评论无需注册,审核通过后可见。