如何在 C# 中识别 CaptchaFox,避免 token 被拒

要在 C# 中识别 CaptchaFox,用 sitekey 和页面 URL 调用 CaptchaFoxAsync,把返回的 token 填入 cf-captcha-response 表单字段,并用 CapSkip 随 token 一起返回的 user agent 发送这个请求。大多数集成就栽在最后这一步。CaptchaFox 的 token 绑定在生成它的浏览器上,而 user agent 不匹配,是正确的 token 遭到拒绝的最常见原因,尽管识别本身是成功的。CapSkip 从 1.4.0 版开始支持 CaptchaFox。本指南讲这几件事:找到 key、调用、提交,以及决定 token 能否被接受的几项设置。
你需要什么
- 在 Windows 机器上运行的 CapSkip 1.4.0 或更高版本。CaptchaFox 支持就是在该版本中加入的,同时加入的还有 Friendly Captcha 和 Capy Puzzle。
- CapSkip .NET 包 1.3.0 或更高版本,CaptchaFoxAsync 就是在这个版本中加入的。它面向 .NET Standard 2.0,并依赖 System.Text.Json 8,因此支持 .NET Framework 4.6.2 及更高版本,以及 .NET 6 及更高版本。示例代码使用了顶级语句和 .NET 6 控制台项目的隐式 using,所以请在 .NET 6 或更高版本上运行。
- 目标页面上的两个值:sitekey,以及小组件所在页面的 URL。第 1 步会告诉你 key 在哪里,页面上还有一个细节决定了一项可选设置。
- 识别工具的地址。Local 模式只在 127.0.0.1 上响应,仅供本机使用;Server 模式监听你的网络地址或公网 IP,这样另一台机器就能通过 API 调用它。两者都位于 连接设置中,下文有一节会说明何时该切换。
# dotnet add package CapSkip dotnet add package CapSkip
第 1 步:找到 sitekey 和小组件来源
在 C# 中识别 CaptchaFox 所需的一切都来自目标页面。sitekey 是公开的,对每个访问者都一样,按惯例以 sk_ 开头。站点会把它放在三个地方之一,你只需找到其中一个。
- 小组件自行渲染时,在容器元素上:一个带有 captchafox 类和 data-sitekey 属性的 div。隐藏模式在表单提交之前什么都不显示,它用的是同一个 div,只是把 data-mode 设为 hidden,所以 key 也在那里。
- 页面用自己的脚本构建小组件时,在 captchafox.render 调用的选项里。render 调用同样可以开启隐藏模式,key 就在它旁边。
- 返回的 HTML 里两者都没有时,在 Network 标签页里。找到发往 api.captchafox.com 的请求;key 就是 /captcha/ 之后的那段路径。
页面源码还开着的时候,顺便看一下加载小组件的 script 标签。小组件有两个来源,站点加载的是哪一个,决定了该站点期望拿回什么格式的 token。
| 页面从哪里加载小组件 | 返回的 token | 需要传入什么 |
|---|---|---|
| https://cdn.captchafox.com/,大多数站点使用的标准小组件 | 普通 token | 无需额外传入;这是默认情况 |
| https://s.uicdn.com/mampkg/ 下的一个包,部分平台会嵌入它 | 以 MAM_ 开头的 token | script 标签里的完整包路径,作为 api_server 选项传入 |
这一步弄错了,失败是无声无息的。如果你传错了来源,识别照样会成功并返回 token,只是格式不被站点接受,所以失败要到后面才以表单被拒的形式出现,而不是一个你能捕获的错误。
第 2 步:调用 CaptchaFoxAsync
这个方法接受 sitekey、页面 URL,以及一个可选的选项字典。对于标准小组件,只需要前两个就够了。
// dotnet add package CapSkip
using CapSkip;
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
// The sitekey from the widget, and the page the widget runs on.
var result = await solver.CaptchaFoxAsync(
"YOUR_SITEKEY",
"https://example.com/signup");
Console.WriteLine(result.Token); // goes in cf-captcha-response
Console.WriteLine(result.UserAgent); // send this as the User-Agent读取 Token 属性。Code 属性里是同一个字符串,但 Token 是按它要填入的字段命名的。UserAgent 是签发该 token 的浏览器的身份,也就是 CapSkip 自己的浏览器,而不是你发送的任何内容。如果某次识别没有报告 user agent,SDK 会让它保持为 null,而不是编造一个值。
与大多数类型相比,页面 URL 在这里更加重要。CaptchaFox 会为每个 key 登记一份允许的域名列表,并在签发任何东西之前检查主机,所以配错页面的 key 会被永久拒绝,而不是偶尔被拒。CapSkip 遇到这种情况会立即报告,而不是重试,因为重试也无济于事。请发送小组件实际运行的那个页面,而不是搜索结果、重定向地址或短链接。
加载 MAM 包的站点
如果第 1 步找到的是 s.uicdn.com 下的脚本,就把它的包路径按页面上的写法原样复制下来,作为 api_server 传入。这样返回的 token 就会带上站点期望的 MAM_ 前缀。
// Only for pages whose script tag loads the MAM build.
var result = await solver.CaptchaFoxAsync(
"YOUR_SITEKEY",
"https://example.com/signup",
new Dictionary<string, object?>
{
["api_server"] =
"https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/",
});选项字典接受 api_server、proxy 和 useragent,另外还有 proxytype 和以秒为单位的单次调用超时。useragent 选项只是为了与其他服务兼容而存在,并不会生效,因为 CapSkip 在真实浏览器中识别,用的是该浏览器自身一致的身份。未知的键,或者为空的 sitekey 或页面 URL,都会在发出请求之前抛出 ValidationException。
第 3 步:用签发 token 的那个 user agent 提交
token 是被接受还是被拒绝,就看这一步。CaptchaFox 会给运行小组件的浏览器打分,它签发的 token 也只属于那个浏览器。除非你自己添加,否则 HttpClient 根本不发送 User-Agent 请求头;而写死一个桌面浏览器的字符串同样不对,所以要把 CapSkip 返回的那个 user agent 复制到携带 token 的请求上。
// http is your HttpClient; result comes from Step 2.
var request = new HttpRequestMessage(HttpMethod.Post, "https://example.com/signup")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
["cf-captcha-response"] = result.Token!,
}),
};
// The token is bound to the browser that produced it.
if (result.UserAgent is { } ua)
request.Headers.TryAddWithoutValidation("User-Agent", ua);
var response = await http.SendAsync(request);使用 TryAddWithoutValidation 是有意为之。更严格的 UserAgent.ParseAdd 会按请求头语法检查字符串,两者不一致时就抛出 FormatException,而这个值无论包含什么,都必须逐字节原样发出。要按请求设置它,而不是设在客户端的默认请求头上,这样一个 HttpClient 就能携带多次识别得到的 token。
另外两条规则,来自 CaptchaFox 在站点一侧校验 token 的方式。CaptchaFox 自己的文档写明: 每个 token 只能验证一次,而且只能在很短的时间内验证,所以要在即将提交时再识别,token 只发送一次;如果表单填到一半被放下、之后又继续,就重新识别。另外,要把 token 当作不透明的数据:它会与生成它的会话进行比对,所以截断或重新编码都会让它失效。有些集成是把它放在 JSON 请求体里发送,而不是通过表单提交,所以请打开 DevTools,手动提交一次表单,然后原样照搬页面发送的内容。
第 4 步:代理、挑战类型和超时
CaptchaFox 不仅给浏览器打分,也给小组件所在的网络打分。用一个地址做测试和偶尔识别没有问题,但从它反复识别,会让这个地址先是招来交互式挑战,继而遭到拒绝。一旦识别有了一定量,就在 CapSkip 里配置代理池,或者为每个请求单独传入代理。这样做的话,提交也要走同一个出口,让 token 和表单从同一个网络到达。CaptchaFox 的验证允许站点连同 token 一起传入访问者的 IP 地址,这又是一个让两者走同一条线路的理由。
// using System.Net; for WebProxy and NetworkCredential.
// Same exit address for the solve and the submit.
var result = await solver.CaptchaFoxAsync("YOUR_SITEKEY", pageUrl,
new Dictionary<string, object?>
{
["proxy"] = new Proxy("HTTP", "login:[email protected]:8080"),
});
var http = new HttpClient(new HttpClientHandler
{
Proxy = new WebProxy("http://1.2.3.4:8080")
{
Credentials = new NetworkCredential("login", "password"),
},
});出现哪种挑战不由你选择。大多数识别根本不会弹出任何挑战,因为浏览器层面的证据本身就足以过关;当 CaptchaFox 要求滑块时,CapSkip 也会完成它。那两种少见的后备挑战则不予识别。
| 挑战类型 | 出现频率 | 已识别 |
|---|---|---|
| 隐形,不显示任何内容 | 通常 | 是 |
| 滑块拼图 | 有时 | 是 |
| 图片选择 | 很少 | 否,报告为无法识别 |
| 音频 | 很少 | 否,报告为无法识别 |
无法识别的报告会以 ApiException 的形式很快返回,而不是等到超时。重试通常会换来另一种挑战,所以把它当作该重新提交的信号,而不是 key 出了问题。下面的完整示例只对这种情况重试两次,其他情况一律不重试,因为 ApiException 也涵盖了重试解决不了的错误,比如 API 密钥错误。
CaptchaFoxAsync 按 recaptchaTimeout 轮询,默认 300 秒,因为它是一个真实的浏览器会话,遇到滑块时耗时更长。CapSkip 自己也有一套计时:一个识别任务最多可以等待 250 秒(Wait Timeout),等 10 个 CaptchaFox 线程(Max. Threads)中的一个空出来,单次尝试则有 150 秒(Row Timeout)。因此,被代理拖慢的尝试会先在 CapSkip 内部失败,把 SDK 的超时调长也不会给它更多时间。在 Retries 保持默认值 0 且有空闲线程时,这个失败会以 ApiException 的形式到达你这里,远早于 SDK 的 300 秒用完;如果识别任务排队很久,SDK 就可能先到达它的时限,改为抛出 CapSkip.TimeoutException。
只有在 CapSkip 的 CaptchaFox 设置中调高 Retries (0-3) 时,才需要把 SDK 的超时调长,因为每多一次重试,单次调用最多会再多出一个 Row Timeout 的时长。这时,在选项字典里传入一个更长的 timeout,或者在构造函数里调高 recaptchaTimeout。
把识别工具放到别处运行
示例使用 127.0.0.1,因为当你的代码和 CapSkip 在同一台机器上时,这样写是对的。一旦 .NET 应用跑在别处,比如容器、构建代理、VPS 或应用服务,回环地址就指向了错误的机器,第一次识别就会抛出 NetworkException。把 CapSkip 切换到 Server 模式,它就会监听你的网络地址或公网 IP,上面这些环境都能通过 API 访问到它。如果链路要经过公网,请使用静态公网 IP,打开 API 密钥校验,并用一条 Windows Firewall 规则把端口限制在你预期的地址上。它仍然是你自己的 Windows 机器,识别也仍然不计量。
客户端不会自己读取环境变量。像完整示例那样,在你自己的代码里读取 CAPSKIP_HOST 和 CAPSKIP_API_KEY 并传给构造函数,这样同一个构建在你的桌面机和服务器上都能运行。
完整可运行示例
// dotnet add package CapSkip
using System.Text.RegularExpressions;
using CapSkip;
// Copied by hand from the script tag, for pages that load the MAM build.
const string MamPackage = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/";
var pageUrl = "https://example.com/signup";
var http = new HttpClient();
var solver = new CapSkipClient(
apiKey: Environment.GetEnvironmentVariable("CAPSKIP_API_KEY") ?? "capskip",
host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
port: 8080);
// Read the sitekey off the captchafox container, in either attribute order.
var html = await http.GetStringAsync(pageUrl);
var widget = Regex.Match(html,
"<[^>]*class=\"(?:[^\"]*\\s)?captchafox(?:\\s[^\"]*)?\"[^>]*>").Value;
var sitekey = Regex.Match(widget, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;
// MAM pages may carry the key in the script src instead.
if (sitekey.Length == 0)
sitekey = Regex.Match(html,
"captchafox[^\"]*/api\\.js\\?key=([^\"&]+)").Groups[1].Value;
if (sitekey.Length == 0)
throw new InvalidOperationException("No sitekey in the HTML; find it in DevTools.");
// The same CDN serves other packages, so match the captchafox one.
var options = new Dictionary<string, object?>();
if (html.Contains("mampkg/@mamdev/core.frontend.libs.captchafox"))
options["api_server"] = MamPackage;
try
{
// Image-select and audio come back unsolvable; a retry redraws.
// Other API errors, such as a wrong key, are not worth repeating.
SolveResult? result = null;
for (var attempt = 1; result is null; attempt++)
{
try
{
result = await solver.CaptchaFoxAsync(sitekey, pageUrl, options);
}
catch (ApiException ex) when (attempt < 3 && ex.Message.Contains("UNSOLVABLE"))
{
Console.WriteLine($"attempt {attempt}: {ex.Message}");
}
}
var request = new HttpRequestMessage(HttpMethod.Post, pageUrl)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
["cf-captcha-response"] = result.Token!,
}),
};
if (result.UserAgent is { } ua)
request.Headers.TryAddWithoutValidation("User-Agent", ua);
// Post wherever the form's action attribute points.
var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode}, UA sent: {result.UserAgent is not null}");
}
catch (CapSkip.ValidationException ex)
{
// An option the method does not take.
Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
Console.WriteLine("gave up waiting; recaptchaTimeout is 300 seconds");
}
catch (CapSkipError ex)
{
// The third unsolvable result, a refused key, or CapSkip unreachable.
Console.WriteLine($"gave up: {ex.Message}");
}这个正则用空白字符界定类名,所以像 captchafox-wrapper 这样的元素不会被误匹配;它还要求属性使用双引号,这与 CaptchaFox 自己的代码片段写法一致。如果每次运行时识别都立刻失败,先检查页面 URL:在登记域名之外使用的 key,每次都会以同样的方式失败。如果正则什么都没匹配到,说明页面是用脚本渲染小组件的,key 在 render 调用里或 Network 标签页里,具体见第 1 步。这个方法背后的原始接口记录在 API 参考文档中;这个包暴露的其他所有方法,则列在 C# 验证码识别页面.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| CapSkip 返回识别成功的 token 被站点拒绝 | 请求发出时用的 User-Agent 与签发 token 时的不同 | 在提交请求上逐字节原样发送 result.UserAgent |
| user agent 一致,却仍被拒绝 | 站点加载的是 MAM 包,识别用的却是默认小组件,或者反过来 | 读取 script 标签,并把 api_server 设成与之匹配 |
| 以 MAM_ 开头的 token 被站点拒绝 | 页面加载的是标准小组件,却设置了 api_server | 去掉这个选项,使用默认值 |
| 某个 key 每次都立刻抛出 ApiException | 页面 URL 不在该 key 登记的域名之内 | 发送小组件所在的页面,而不是重定向地址或搜索结果 |
| 偶尔抛出 ApiException,提示验证码无法识别 | CaptchaFox 弹出了图片选择或音频挑战 | 重新提交;下一次尝试通常会换成别的挑战 |
| 随着运行持续,先是挑战增多,然后开始被拒 | 每次识别都来自同一个地址,而 CaptchaFox 会给网络打分 | 通过代理池分散识别请求,并从同一个出口提交 |
| 第一次能用的 token,第二次提交时失败 | 每个 token 只能验证一次,而且很快过期 | 每次提交都重新识别,并且识别完立刻提交 |
| 还没发送任何内容就抛出 ValidationException | sitekey 或页面 URL 为空,或者选项里有这个方法不接受的键 | 确认正则找到了该元素,并去掉错误信息中指出的那个选项 |
| 第一次识别时抛出 NetworkException | CapSkip 没有在运行,或者主机和端口不对 | 启动 CapSkip,然后确认它应该处于 Local 模式还是 Server 模式 |
| 编译失败,报 TimeoutException 有歧义 | CapSkip 和 System 都定义了这个短名称 | 把 CapSkip.TimeoutException 写全,或者改为捕获 CapSkipError |
常见问题
为什么大多数验证码类型都不需要 user agent,CaptchaFox 却需要?
因为它是给浏览器打分,而不是让人去辨认什么。token 是该服务对某一个特定浏览器做出的判定,所以只有从那个浏览器发回来才有意义。Cloudflare Turnstile 挑战页面也是同样的机制,C# 这一侧的做法见 Turnstile 挑战页面指南。CapSkip 恰好只为这两种类型返回 user agent,这可以可靠地提示你,它在哪些场合真正要紧。
跑在托管平台上的 .NET 程序能连到识别工具吗?
可以。在连接设置里把 CapSkip 切换到 Server 模式,让它监听网络地址而不是回环地址,然后在你的代码里从 CAPSKIP_HOST 读取这个地址,并传给客户端。容器宿主机、VPS、CI 代理和托管应用服务的连接方式都一样,走的是同一套 HTTP API。如果链路要经过公网,请使用静态公网 IP 并配上防火墙规则。识别工具始终运行在你自己的硬件上,因此授权和识别次数都不会有任何变化。
这和在 C# 中识别 Friendly Captcha 有什么不同?
两者都是在 CapSkip 1.4.0 中加入的,都使用 300 秒超时,相同之处基本就这些。对于 Friendly Captcha,要判断的是站点运行哪个协议版本,而且 token 不绑定 user agent。对于 CaptchaFox,要判断的是站点加载哪个小组件来源,以及用哪个 user agent 携带 token。Friendly Captcha 这一侧的内容见 C# Friendly Captcha 指南.
我可以同时识别多个 CaptchaFox token 吗?
可以。客户端是异步的,所以对几次 CaptchaFoxAsync 调用使用 Task.WhenAll,就能让它们并行执行。CapSkip 默认同时运行 10 个 CaptchaFox 识别(CaptchaFox 设置中的 Max. Threads),其余的会等待空闲线程,最多等到 250 秒的 Wait Timeout,所以在正常识别耗时下,一批五十个调用没有问题;批量更大时,就调高 Max. Threads,或者把调用分成更小的组发送。另外还有两点需要提前考虑:每个 token 都和它自己的 user agent 配对,所以要把识别结果和它所属的请求放在一起;从同一个地址并发,会更快地推高挑战率,所以每增加一些并行识别,就要相应地增加代理。
简短版结论
要在 C# 中识别 CaptchaFox,先从 captchafox 容器、render 调用或 Network 标签页读取 sitekey,并检查是哪个脚本加载了小组件。用 sitekey 和真实的页面 URL 调用 CaptchaFoxAsync,只有遇到 MAM 包时才加上 api_server。以 result.UserAgent 为 user agent,把 result.Token 放进 cf-captcha-response 提交,只提交一次,并且立刻提交;如果返回的是无法识别的挑战,就重新提交。用量增长时加上代理,一旦调用方代码离开识别工具所在的机器,就切换到 Server 模式。
- 这个类型如何运作、识别工具覆盖哪些内容: CaptchaFox 识别页面.
- .NET 包暴露的其他所有方法: C# 与 .NET 验证码识别页面.
最后还有一点,它决定了你该如何重试。当一次识别因图片选择挑战而被退回时,解决办法就是再试一次;而当 无限量验证码识别工具 就运行在你自己的机器上时,这次重试只花几秒钟,而不是再多一次计费的识别。
