如何在 C# 中识别 Friendly Captcha(v1 与 v2 小组件)

要在 C# 中识别 Friendly Captcha,用 sitekey、页面 URL 和小组件版本调用 FriendlyCaptchaAsync,然后把 token 提交到该版本要求的表单字段里。成败全在版本上。Friendly Captcha 在同一个名字下发布了两套不同的协议,而且它们共用同一个 sitekey 命名空间,所以从 key 本身看不出一个站点用的是哪一套。识别错了版本,你会拿到一个格式完好的 token,站点却会不声不响地拒绝它。CapSkip 从 1.4.0 版开始支持 Friendly Captcha。本指南讲三件事:如何区分两个版本、如何调用,以及如何提交。
你需要什么
- 在 Windows 机器上运行的 CapSkip 1.4.0 或更高版本。Friendly Captcha 支持就是在该版本中加入的,同时加入的还有 CaptchaFox 和 Capy Puzzle。
- CapSkip .NET 包 1.3.0 或更高版本,FriendlyCaptchaAsync 就是在这个版本中加入的。它面向 .NET Standard 2.0,因此 .NET Framework 4.6.1 及以上、.NET Core 2.0 及以上,以及 .NET 6 及更高版本都能使用。示例代码使用了顶级语句,所以请在 .NET 6 或更高版本上运行。
- 目标页面上的三个值:小组件元素上的 sitekey、页面本身的 URL,以及页面加载的小组件脚本地址。第 1 步会告诉你每个值在哪里。
- 识别工具的地址。Local 模式只在 127.0.0.1 上响应,仅供本机使用;Server 模式监听你的网络地址或公网 IP,这样另一台机器就能通过 API 调用它。两者都位于 连接设置中,第 4 步会说明你需要哪一种。
# dotnet add package CapSkip dotnet add package CapSkip
第 1 步:区分 Friendly Captcha v1 和 v2
两个版本渲染出的元素完全相同:一个带有 frc-captcha 类和 data-sitekey 属性的 div。所以这个元素只能告诉你 sitekey 在哪里,却说明不了用的是哪套协议。真正露馅的是加载小组件的 script 标签,因为 v1 和 v2 是两个不同的包,文件名也不同。
| 对比项 | 版本 1 | 版本 2 |
|---|---|---|
| 页面加载的包 | friendly-challenge | @friendlycaptcha/sdk |
| 脚本文件 | widget.module.min.js,以 widget.min.js 作为后备 | site.min.js,以 site.compat.min.js 作为后备 |
| token 要填入的表单字段 | frc-captcha-solution | frc-captcha-response |
| token 长什么样 | 由点号分隔的四段,长度为几百个字符 | 一整段不透明的字符串,以 AQQA 加一个点号开头,大约 6 KB |
查看页面源码,搜索 frc-captcha 找到该元素,然后看加载它的 script 标签。Friendly Captcha 在 自家介绍两个版本的页面上也写了同样的判断方法。如果站点自行托管小组件,URL 里可能就看不到包名了,这时改看文件名。不要根据站点的年头去猜:两个版本都还在线上使用,而且 Friendly Captcha 表示会在未来几年里继续维护 v1。
第 2 步:调用 FriendlyCaptchaAsync
一个方法,三个参数:sitekey、页面 URL,以及一个选项字典。把你在第 1 步中找到的版本传进去。
// dotnet add package CapSkip
using CapSkip;
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
// Tell the solver which protocol the site runs.
var result = await solver.FriendlyCaptchaAsync(
"YOUR_SITEKEY",
"https://example.com/signup",
new Dictionary<string, object?> { ["version"] = "v2" });
Console.WriteLine(result.Token); // goes in frc-captcha-responseversion 选项接受 v1 或 v2,直接写 1 或 2 也可以。对于其他任何值,SDK 都会在发出请求之前抛出 ValidationException 拒绝它。这是有意为之:猜出来的版本会换回一个看起来有效、却被站点直接丢弃的 token,这比报错更糟。不过,null 版本不会被拒绝。它会被直接忽略,识别工具随后按下文所述回退到 v1。
读取 Token 属性。Code 属性里是同一个字符串,但 Token 是按它要填入的字段命名的。这里的 user agent 保持为 null,因为只有 Turnstile 和 CaptchaFox 会返回它。
让脚本地址来决定版本
如果你的代码已经拿到了小组件脚本的地址,就改为传入这个地址,CapSkip 会从站点实际加载的构建里读出版本,这是最可靠的依据。识别工具按固定顺序检查,拿到第一个答案就停下:先看 version 选项,再看脚本地址,最后默认为 v1。
// Instead of the call above: copy the src straight off the page's module script tag.
var result = await solver.FriendlyCaptchaAsync(
"YOUR_SITEKEY",
"https://example.com/signup",
new Dictionary<string, object?>
{
["module_script"] =
"https://cdn.jsdelivr.net/npm/@friendlycaptcha/[email protected]/site.min.js",
});如果你手上的是后备脚本,nomodule_script 选项可以用它完成同样的事。不过要留意这个顺序的最后一步。既没有版本也没有脚本地址时,识别工具会假定是 v1,所以只凭一个 sitekey 去识别 v2 站点,就会恰好以本指南一直在警告的方式失败。
使用 EU 端点的站点
Friendly Captcha 提供一项付费的数据驻留选项,只从德国提供谜题。使用该服务的 v2 小组件带有 data-api-endpoint 属性,通常设为 eu;看到它时,把同样的值作为 api_server 选项传入。这个选项接受 global(默认值)、eu 或一个完整的 URL。两个端点都会为同一个 sitekey 签发 token,所以一旦弄错,只有站点自己的验证才会发现,这又是那种无声无息的失败。
第 3 步:把 token 提交到正确的字段
字段名是第二个让人栽跟头的地方,通常发生在一个本来好好的 v1 集成被拿去对付 v2 站点之后。小组件会把 token 写进一个隐藏的 input,你的请求也必须把它放在同一个地方。
// version holds what you passed in Step 2; http is your HttpClient.
// v1 reads frc-captcha-solution, v2 reads frc-captcha-response.
var isV2 = version is "v2" or "V2" or "2";
var field = isV2 ? "frc-captcha-response" : "frc-captcha-solution";
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
[field] = result.Token!,
});
var response = await http.PostAsync("https://example.com/signup", body);在此之上还有两个细节。站点可以通过小组件元素上的一个属性给字段改名,v2 上是 data-form-field-name,v1 上是 data-solution-field-name,所以要检查一下,如果有,就用它的值。另外,有些集成是把 token 放在 JSON 请求体里发送,而不是通过表单提交,所以请打开 DevTools,手动提交一次表单,然后原样照搬页面发送的内容。
对 v2 来说,大小很关键。大约 6 KB 的 token 放在 POST 请求体里没问题,但它可能超出服务器对查询字符串的长度限制,或者被一个太窄的数据库列截断,而被截断的 token 会像错误的 token 一样验证失败。保持它完整,只发送一次。在两个版本中,Friendly Captcha 的验证都会拒绝已过期或已被使用过的响应,所以一个 token 只够提交一次,而且应尽快发出。
第 4 步:超时,以及识别工具运行在哪里
Friendly Captcha 属于工作量证明,但不是毫秒级的那种。服务端会在每个请求发出的那一刻决定它值多少工作量,而对已经见过很多次的地址,它还会调高这个数字。另外,CapSkip 会在真实浏览器里识别每一个 v2 小组件。所以这个方法用的是更长的 reCAPTCHA 轮询超时,而不是默认超时。
| 构造函数选项 | 默认值 | 它的适用范围 |
|---|---|---|
| recaptchaTimeout | 300 秒 | Friendly Captcha、CaptchaFox、reCAPTCHA 与极验(GeeTest)的轮询 |
| defaultTimeout | 120 秒 | 图片验证码、ALTCHA 与 Capy 的轮询 |
| pollingInterval | 最长 5 秒 | 轮询从 0.25 秒开始,逐步退避到这个值 |
如果在长时间运行中识别越来越慢,多半就是难度在上升,解决办法是用更多的地址。这个方法支持为每个请求单独指定代理,而在 CapSkip 里配置的代理池会替你分摊负载。与大多数类型相比,在这里更值得早点把这两样准备好。
示例使用 127.0.0.1,因为当你的代码和识别工具在同一台机器上时,这样写是对的。一旦调用方代码跑在别处,比如容器、构建代理或 VPS,回环地址就指向了错误的机器,第一次识别就会抛出 NetworkException。把 CapSkip 切换到 Server 模式,它就会改为监听你的网络地址或公网 IP,上面这些环境都能通过 API 访问到它。如果链路要经过公网,请使用静态公网 IP,并为你预期的地址配置防火墙规则。硬件仍然是你自己的,用量也仍然不计量。
客户端不会自己读取环境变量。像完整示例那样,在你自己的代码里读取 CAPSKIP_HOST 并传给构造函数,这样同一个构建在你的桌面机和服务器上都能用。
完整可运行示例
// dotnet add package CapSkip
using System.Text.RegularExpressions;
using CapSkip;
var pageUrl = "https://example.com/signup";
var http = new HttpClient();
var solver = new CapSkipClient(
host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
port: 8080);
// Read the sitekey off the widget element, in either attribute order.
var html = await http.GetStringAsync(pageUrl);
var widget = Regex.Match(html,
"<[^>]*class=\"(?:[^\"]*\\s)?frc-captcha(?:\\s[^\"]*)?\"[^>]*>").Value;
var sitekey = Regex.Match(widget, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;
// The package name in the script URL decides the version.
var v2 = html.Contains("@friendlycaptcha/sdk");
var v1 = html.Contains("friendly-challenge");
if (v1 == v2)
throw new InvalidOperationException("Read the script tag and set the version by hand.");
var version = v2 ? "v2" : "v1";
try
{
var result = await solver.FriendlyCaptchaAsync(sitekey, pageUrl,
new Dictionary<string, object?> { ["version"] = version });
// A site can rename the field on the widget element.
var renamed = Regex.Match(widget,
"data-(?:form|solution)-field-name=\"([^\"]+)\"").Groups[1].Value;
var field = renamed.Length > 0 ? renamed
: v2 ? "frc-captcha-response" : "frc-captcha-solution";
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
[field] = result.Token!,
});
// Post wherever the form's action attribute points.
var response = await http.PostAsync(pageUrl, body);
Console.WriteLine($"{(int)response.StatusCode} with a {version} token");
}
catch (CapSkip.ValidationException ex)
{
// An empty sitekey or an unknown version, refused before any request.
Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
Console.WriteLine("gave up waiting; recaptchaTimeout is 300 seconds");
}版本只判定一次,却用在两处:识别和字段名,所以两者永远不会对不上。如果正则什么都没匹配到,通常是因为小组件由 JavaScript 动态创建,而不是直接写在 HTML 里,这时你需要从渲染后的页面或者创建它的脚本里读取 sitekey。这个方法背后的原始接口及其接受的全部参数,都记录在 API 参考文档中;这个包暴露的其他所有方法,则列在 C# 验证码识别页面.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| CapSkip 返回识别成功的 token 被站点拒绝 | 识别了错误的版本,常见情况是对 v2 站点套用了默认的 v1 | 读取 script 标签并传入版本,或者直接传入脚本地址 |
| 版本正确,token 却仍被站点拒绝 | token 填进了另一个版本的字段,或者站点给字段改了名 | 使用该版本对应的字段,或者使用 v2 上 data-form-field-name、v1 上 data-solution-field-name 中给出的名字 |
| 站点拒绝 token,而小组件设置了 data-api-endpoint | 站点运行在区域端点上,而识别用的是默认端点 | 把该属性的值作为 api_server 选项传入 |
| 还没发送任何内容就抛出 ValidationException | sitekey 或页面 URL 为空,版本不是 v1、v2、1 或 2,或者选项里有这个方法不接受的键 | 确认正则找到了该元素,修正版本值,并去掉未知的选项 |
| 下载到的 HTML 里没有 frc-captcha 元素 | 页面是用 JavaScript 创建小组件的 | 从渲染后的页面或者构建它的脚本里读取 sitekey |
| 第一次能用的 token,第二次提交时失败 | Friendly Captcha 每个响应只接受一次,而且响应会过期 | 每次提交都重新识别,并且识别完立刻提交 |
| 长时间运行时,识别越来越慢 | 对于见过很多次的地址,服务端会提高工作量 | 通过代理池分散识别请求 |
| 第一次识别时抛出 NetworkException | CapSkip 没有在运行,或者主机和端口不对 | 启动 CapSkip,然后确认它应该处于 Local 模式还是 Server 模式 |
| 编译失败,报 TimeoutException 有歧义 | CapSkip 和 System 都定义了这个短名称 | 把 CapSkip.TimeoutException 写全,或者改为捕获 CapSkipError |
常见问题
在 C# 中识别 Friendly Captcha,我这边需要浏览器吗?
不需要。你的代码只需要一个 HttpClient,别的什么都不用。CapSkip 会在它所运行的机器上完成工作,对 v2 来说,就是在那台机器上的真实浏览器里运行小组件,这也是这个方法使用更长超时的原因之一。你这边只需发送 sitekey、URL 和版本,拿回一个要提交的字符串,所以它可以轻松地跑在后台工作服务或定时任务里。
这和识别 ALTCHA 有什么不同?
两者都是工作量证明,相似之处也仅此而已。对于 ALTCHA,你交出的是挑战本身或它的来源接口,哈希计算只需几毫秒,方法走的是 120 秒的默认超时。对于 Friendly Captcha,服务端按每个请求设定难度,并对频繁访问的地址调高难度,v2 在浏览器中识别,方法走的是 300 秒超时。另外,ALTCHA 只有一个字段名,而 Friendly Captcha 有两个。ALTCHA 这一侧的内容请参阅 C# ALTCHA 指南.
跑在托管平台上的 .NET 程序能连到识别工具吗?
可以。在连接设置里把 CapSkip 切换到 Server 模式,让它监听网络地址而不是回环地址,然后在你的代码里从 CAPSKIP_HOST 读取这个地址,并传给客户端。容器宿主机、VPS、CI 代理和托管应用服务的连接方式都一样,走的是同一套 HTTP API。如果链路要经过公网,请使用静态公网 IP 并配上防火墙规则。识别工具始终运行在你自己的硬件上,因此授权和识别次数都不会有任何变化。
我可以提前批量识别一批 token 吗?
可以,但没有意义。每个响应只被接受一次,而且会过期,而这两种情况 Friendly Captcha 都会作为失败报告给站点。囤起来的 token 最后只会变成一堆拒绝。要在即将提交时再识别,改用并发来提高效率:客户端是异步的,所以对几次识别调用 Task.WhenAll,就能让它们并行执行。
简短版结论
要在 C# 中识别 Friendly Captcha,先找到小组件脚本,据此判断是 v1 还是 v2,然后把这个版本连同 sitekey 和页面 URL 一起传给 FriendlyCaptchaAsync。除非站点给字段改了名,否则 v1 的 token 填入 frc-captcha-solution,v2 的填入 frc-captcha-response,并且立刻提交,只提交一次。如果小组件使用 EU 端点,就要与之匹配;识别耗时要按秒而不是按毫秒来预期;一旦调用方代码离开识别工具所在的机器,就立刻切换到 Server 模式。
- 这个类型如何运作、识别工具覆盖哪些内容: Friendly Captcha 识别页面.
- .NET 包暴露的其他所有方法: C# 与 .NET 验证码识别页面.
最后还有一点,它决定了你该如何重试。当 token 被拒绝时,最实在的解决办法几乎总是用正确的版本重新识别一次;而用 本地验证码识别工具 的话,这次重试只花掉你自有机器上的几秒钟,而不是在别人的账单上再添一行。
