如何在 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,因此可在 .NET Framework 4.6.1+、
# .NET Core 2.0+ 以及 .NET 6 到 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 */ }

TimeoutExceptionValidationException 在两个命名空间中都存在 SystemCapSkip。当同时导入这两个命名空间时,未加限定的 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 是一个两参数调用。挑战页面需要 datapagedata 从页面实时读取,并且 token 必须连同随其一起返回的 user agent 一起提交。请捕获 CapSkipError ,而不是与命名空间冲突较劲;当挑战对地理位置敏感时,请使用代理。

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