如何在 C# 中识别 Cloudflare Turnstile 挑战页面

turnstile challenge page - How to Solve Cloudflare Turnstile Challenge Pages in C#

Cloudflare Turnstile 有两种形态,两者对应的 C# 代码各不相同。嵌入表单中的小组件只需要一个 sitekey 和一个 URL。而整页插页式挑战则需要从页面中额外提取两个值,并且只有当你回传识别工具所使用的 user agent 时才会被接受。漏掉最后这一步,你就会得到一个看起来完美无缺、却每次都验证失败的 token。

本指南涵盖这两种情况,并提供可运行的 .NET 代码。

小组件还是挑战页面?

在编写任何代码之前,先弄清楚你面对的是哪一种。

小组件模式挑战页面
你所看到的表单中的一个复选框,且表单仍可正常交互整页的插页式拦截,通常显示“正在检查您的浏览器”
页面其余部分正常加载在挑战通过之前被阻止
需要 data / pagedata否是
需要回传的 user agent否是

如果你不确定,我们的 Turnstile 在线演示页面 运行的是小组件版本,因此你可以用它与你实际遇到的情况进行对比。

设置

CapSkip 运行在你自己的机器上,因此在开始之前需要先启动一个本地服务。从 NuGet 安装 SDK:

# .NET Standard 2.0, so this works on .NET Framework 4.6.1+,
# .NET Core 2.0+, and .NET 6 through 9.
dotnet add package CapSkip

然后将客户端指向 CapSkip 应用中显示的端口:

using CapSkip;

var solver = new CapSkipClient(
    apiKey: "capskip",        // any string when key validation is off
    host: "127.0.0.1",
    port: 8080,
    recaptchaTimeout: 300);   // seconds, also covers Turnstile and GeeTest

小组件模式,简单情形

两个参数即可搞定:

var result = await solver.TurnstileAsync(
    "0x4AAAAAAA...",                  // sitekey from the data-sitekey attribute
    "https://example.com/login");

Console.WriteLine(result.Code);       // cf-turnstile-response token

把返回的 token 放入 result.Code 放入 cf-turnstile-response 字段并提交表单,无需再做任何操作。

挑战页面需要另外两个值

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

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

这两个值都嵌入在挑战页面本身之中,而不是在某个表单属性里,因此你必须先读取页面才能进行识别。在标准的 Cloudflare 插页式拦截中,它们与 sitekey 一起暴露在页面自身的挑战选项对象上。

它们也是一次性的,且与该特定页面加载绑定。获取后应立即识别,切勿在多次尝试之间缓存。

using System.Collections.Generic;
using CapSkip;

var result = await solver.TurnstileAsync(
    sitekey,
    pageUrl,
    new Dictionary<string, object?>
    {
        ["data"]     = cData,          // the cData value from the page
        ["pagedata"] = chlPageData,    // the chlPageData value
        ["action"]   = "managed",       // optional, when the page declares one
    });

Console.WriteLine(result.Code);
Console.WriteLine(result.UserAgent);   // you are going to need this

人人都会忽略的部分:user agent

Turnstile 将 token 绑定到生成它的浏览器指纹上,而 user agent 正是其中的一部分。CapSkip 会返回它所使用的那个,位于 result.UserAgent中。如果你随后从一个 HttpClient 发送其自身默认 user agent 的情况下提交该 token,两者的值就不匹配,Cloudflare 会拒绝一个本来完全有效的 token。

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

using System.Net.Http;
using CapSkip;

var result = await solver.TurnstileAsync(sitekey, pageUrl, options);

using var http = new HttpClient();

// Send back the exact user agent the solve was performed with.
http.DefaultRequestHeaders.UserAgent.ParseAdd(result.UserAgent);

var form = new FormUrlEncodedContent(new[]
{
    new KeyValuePair<string, string>("cf-turnstile-response", result.Code),
});

var response = await http.PostAsync(pageUrl, form);

如果你的 token 被拒绝,且你已排除过期的 cData,那几乎肯定就是原因所在。

正确处理失败

每个 SDK 异常都派生自 CapSkipError,因此你可以只捕获那一种类型,也可以分别处理每一种。有一个 .NET 特有的陷阱值得了解。

using System;
using CapSkip;

try
{
    var result = await solver.TurnstileAsync(sitekey, pageUrl, options);
}
catch (CapSkip.ValidationException)   { /* bad parameters */ }
catch (NetworkException)              { /* CapSkip is not running */ }
catch (ApiException)                  { /* API returned an error code */ }
catch (CapSkip.TimeoutException)      { /* exceeded recaptchaTimeout */ }
catch (CapSkipError)                  { /* anything else from the SDK */ }

TimeoutException 和 ValidationException 在两个命名空间中都存在 System 和 CapSkip。当同时导入这两个命名空间时,未加限定的 catch (TimeoutException) 会解析为 System 那一个,并且会静默地永远不触发。请像上面那样加以限定,或者直接捕获 CapSkipError 并检查它。

同时识别多个

每个方法都返回一个 Task,因此适用普通的 .NET 并发处理:

var results = await Task.WhenAll(
    solver.TurnstileAsync(sitekeyA, "https://a.example.com"),
    solver.TurnstileAsync(sitekeyB, "https://b.example.com"));

foreach (var r in results)
{
    Console.WriteLine(r.Code);
}

该 SDK 还导出了 AsyncCapSkip,但在 .NET 中它只是以下类型的别名: CapSkipClient。它的存在是为了让从 Python SDK 移植过来的代码仍能编译。切换到它不会带来任何好处,因为 .NET I/O 本来就是异步的。

通过代理路由

如果挑战对地理位置敏感,请从你将用于提交的同一网络路径进行识别:

var result = await solver.TurnstileAsync(sitekey, pageUrl,
    new Dictionary<string, object?>
    {
        ["data"]     = cData,
        ["pagedata"] = chlPageData,
        ["proxy"]    = new Proxy("HTTPS", "user:[email protected]:3128"),
    });

Turnstile、reCAPTCHA 和极验(GeeTest)支持使用代理。图片验证码不支持代理,因为它们是根据图片字节进行识别的,完全不会接触目标网站。

常见问题

我是否总是需要 cData 和 chlPageData?

不需要。只有整页插页式挑战才需要。嵌入表单中的 Turnstile 小组件只需要 sitekey 和页面 URL,在小组件上传入空值只会导致识别失败,而不会有任何帮助。

我的 token 有效,但网站仍然拒绝它。为什么?

几乎总是 user agent。Turnstile 会将 token 与生成它的指纹绑定,因此你必须使用以下位置中的值来提交: result.UserAgent。第二常见的原因是过期的 cData,它与单次页面加载绑定。

这在 .NET Framework 上能用吗?

可以。该包面向 .NET Standard 2.0,因此可运行于 .NET Framework 4.6.1 及更高版本、.NET Core 2.0+ 以及所有现代 .NET 版本。在同步代码中,你可以调用 .GetAwaiter().GetResult(),不过正确地使用 await 更好。

识别一个 Turnstile 大约需要多长时间?

通常只需几秒。客户端会在内部轮询,从 250 毫秒开始,逐步退避到 pollingInterval,并在达到 recaptchaTimeout 时放弃,该值默认为 300 秒。如果你总是触及这一上限,请检查 CapSkip 服务是否确实在运行,而不是一味调高超时时间。

小结

小组件版 Turnstile 是一个两参数调用。挑战页面需要 data 和 pagedata 从页面实时读取,并且 token 必须连同随其一起返回的 user agent 一起提交。请捕获 CapSkipError ,而不是与命名空间冲突较劲;当挑战对地理位置敏感时,请使用代理。

.NET 的完整方法列表见 C# 验证码识别 页面,参数参考见 API 文档,以及 Turnstile 支持 涵盖了其他语言。CapSkip 本身是一个 无限量验证码识别工具 ,它在本地运行,因此以上所有操作都不会按次收费。