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

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 */ }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 本身是一个 无限量验证码识别工具 ,它在本地运行,因此以上所有操作都不会按次收费。
