如何在 C# 中用 HCaptchaAsync 识别 hCaptcha(.NET 8+)

要在 C# 中识别 hCaptcha,请用小组件的 sitekey 和页面地址调用 CapSkip NuGet 包中的 HCaptchaAsync。返回的 SolveResult 包含 token、respKey 和获得 token 时所用的 User-Agent,你要带着 token,在该 User-Agent 下提交表单。同一个方法覆盖复选框、隐形和 Enterprise 小组件。这个调用会与 CapSkip hCaptcha 识别工具 通信,它运行在你自己的 Windows PC 上,统一授权定价意味着再试一次不用花钱。
你需要什么
- 一台运行 CapSkip 4.0.0 或更高版本的 Windows PC,该版本新增了 hCaptcha。安装步骤请参阅 设置指南.
- 需要 .NET 8 SDK 或更高版本,以及 CapSkip NuGet 包 1.6.0 或更高版本(第一个带有 HCaptchaAsync 的版本)。该包的目标框架是 netstandard2.0,依赖 System.Text.Json 8.0.5。我只装了一个 SDK,10.0.100,并且只在 net10.0 和 net8.0 上用 CapSkip 1.6.0 运行了所有内容。
- 一个 PowerShell 提示符,以及一个你被允许自动化操作、显示着 hCaptcha 小组件的页面,连同它的 sitekey。
- CapSkip 的地址。Local 模式只在 127.0.0.1 上响应,供该 PC 上的程序使用。Server 模式还会监听你的网络地址或公网 IP,所以另一台机器上的程序可以调用同一套 API。这个选择保存在 连接设置 中。
测试环境:来自 NuGet 的真实 CapSkip 1.6.0 包,加上我自己写的两个模拟服务器,没有使用真实的 hCaptcha 站点。其中一个模拟服务器代替 CapSkip 应用,并遵循 API 文档中 hCaptcha 部分的内容。另一个扮演注册页面,也可以充当代理。它的规则(会话 cookie、Referer 或 Origin、与 token 配套的 User-Agent)是我自己的选择,不是 hCaptcha 官方的规则。官方的 开发者文档 说明了真实站点如何检查 token。我读了该包在 v1.6.0 标签下的源码,没有向真实的 CapSkip 应用发送任何东西,所以我没有真实的识别耗时。
第 1 步:创建项目并安装 CapSkip
创建一个控制台项目并安装这个包。新项目会启用隐式 using 和可空引用类型,而这段代码两者都需要。不带版本号时,dotnet add package 会取最新版本,我运行时是 1.6.0。
dotnet new console -o hcaptcha-demo cd hcaptcha-demo dotnet add package CapSkip
删掉 Program.cs 里的所有内容,包括模板里的 Console.WriteLine 那一行,然后按顺序粘贴下面三个 C# 代码块,从第 2 步开始。只有第一个代码块有 using 行,最后一个代码块就位之前什么都不会运行。
第 2 步:调用 HCaptchaAsync
sitekey 就在页面上。大多数页面把它作为 data-sitekey 打印在小组件容器上,由脚本构建的小组件则把它作为 hcaptcha.render() 的 sitekey 选项传入。HCaptchaAsync 接受 sitekey、页面地址、一个选项字典和一个 CancellationToken。它的带类型重载在选项之前接受一个名为 invisible 的 bool:true 会发送 invisible=1,false 则什么都不发送。设置了 rqdata 的 Enterprise 站点,其值放在键 data 下,每个任务都要用新的。代理是一个 Proxy 对象,带有 type(HTTP、HTTPS、SOCKS5 或 SOCKS5H)和一个 login:password@host:port 字符串。请把端口写出来,因为 Uri.Authority 会丢掉默认端口。在模拟运行中,invisible、data 和代理都到达了 in.php。
using System.Net;
using System.Text.RegularExpressions;
using CapSkip;
using SolverTimeout = CapSkip.TimeoutException; // System.TimeoutException is in scope too (CS0104)
static class Solve
{
// Local mode. In Server mode, use your server's address instead. If API Key
// Validation is on in CapSkip, also pass apiKey: "YOUR_KEY" here.
static readonly CapSkipClient Client = new(host: "127.0.0.1", port: 8080);
public static Task<SolveResult> Run(string siteKey, string pageUrl, bool invisible,
string? rqData, Uri? proxy, CancellationToken ct)
{
var options = new Dictionary<string, object?>();
if (rqData is not null) options["data"] = rqData; // Enterprise rqdata, fresh per task
if (proxy is not null)
{
// Uri.Authority drops a default port such as 80, so build host:port by hand.
var login = proxy.UserInfo.Length > 0 ? Uri.UnescapeDataString(proxy.UserInfo) + "@" : "";
options["proxy"] = new Proxy(proxy.Scheme.ToUpperInvariant(), $"{login}{proxy.Host}:{proxy.Port}");
}
// invisible: false sends nothing, true sends invisible=1.
return Client.HCaptchaAsync(siteKey, pageUrl, invisible, options, ct);
}
}结果有四个值得了解的属性。Token 和 Code 是同一个字符串。RespKey 是 hCaptcha 交给小组件的第二个值,也就是浏览器里 hcaptcha.getRespKey() 返回的那个。大多数站点从不读取它,而且没有时它是空字符串,不是 null。UserAgent 是获得该 token 的浏览器的 User-Agent 字符串。如果回复里没有指明,它会保持为 null,这时第 3 步不会发送 User-Agent,我的模拟站点就拒绝了这次提交。
SDK 会在任何请求之前检查你的参数。空的 sitekey、不是绝对 http 或 https 的页面地址、不是 0、1、true 或 false 的 invisible 值,以及 CapSkip 的 hCaptcha API 不接受的任何参数(包括 2Captcha 的 domain),都会抛出 ValidationException,模拟服务器对其中任何一种都没有收到任务。否则 SDK 会提交任务并立刻轮询,0.25 秒后再轮询一次,之后间隔翻倍,最长 5 秒。在模拟服务器上,各次轮询出现在提交后 0、0.25、0.75、1.75、3.75、7.75 和 12.75 秒的 0.15 秒误差范围内。
等待在 RecaptchaTimeout 之后以 TimeoutException 结束,默认 300 秒,除非你向构造函数传入 recaptchaTimeout,或向某次调用传入 timeout 选项。hCaptcha 得到这个较长的超时,是因为小组件要在真实浏览器里通过,而文档说一次识别通常需要 10 到 30 秒。截止时间是在两次轮询之间检查的:超时设为 22 秒、模拟服务器始终不回答时,异常在调用后 22.9 秒出现。
SDK 抛出的每个异常都继承 CapSkipError。ValidationException 是本地拒绝。NetworkException 表示请求失败,或者 CapSkip 没有以 HTTP 200 应答。ApiException 表示 CapSkip 回复了错误,它的消息就是整个回复正文(在模拟服务器上,轮询之后是 JSON,提交之后是纯文本),所以错误码就在其中。接下来有两个陷阱。System 里也有 TimeoutException,而隐式 using 会导入 System,所以在 using CapSkip 旁边直接写 TimeoutException 会因 CS0104(不明确的引用)而失败。第一个代码块把 SDK 的这个类以别名 SolverTimeout 引入。此外,在提交期间触发的 CancellationToken 会表现为包装了取消的 NetworkException,而在轮询期间触发的则表现为 TaskCanceledException。
第 3 步:用独立的 handler 提交表单
该包为 CapSkip 保留了自己的 HttpClient,所以下面的内容都不会共享它。表单提交则不同,因为 cookie jar 属于 handler,两次注册不能共享 cookie。Site.SignUp 会构建一个带有自己 CookieContainer 的 handler,在它外面包一个客户端,并在结束时把两者都释放。微软的 HttpClient 使用指南 把使用独立的 cookie 容器列为需要多个实例的理由之一。
先加载页面,这样页面的 cookie 会随提交一起带回去。两个请求都带有结果中的 User-Agent。HttpClient 除非你设置,否则不发送它:去掉那一行之后,会拿它与 token 对照检查的模拟站点看到的是空值,并拒绝了每一次提交。提交时还带上 Referer 和 Origin,因为 HttpClient 两者都不发送。我的模拟站点要求二者有其一,这条规则是我定的,我没有测量有多少真实站点这样要求。token 要填入 h-captcha-response 和 g-recaptcha-response。它只能用一次,按文档所说,约两分钟有效,所以要立即提交。
代理是可选的,不过文档警告说,来自同一个 IP 的持续识别请求,正是 hCaptcha 的速率限制所要捕捉的。你传入的 Uri 会通过 Solve.Run 交给 CapSkip,也交给这个 handler,因为文档要求严格的站点从与识别相同的地址提交表单。WebProxy 会忽略地址中的登录信息,所以 handler 把它提取到 NetworkCredential 里。一个要求 Basic 凭据的模拟代理接受了它,哪怕密码里带有百分号。我没有让站点那一段走 SOCKS 运行过。
static class Site
{
// One HttpClient per sign-up, because the cookie jar belongs to the handler and two
// sign-ups must not share cookies. Dispose it when the sign-up is over.
public static async Task<(HttpStatusCode Status, string Body)> SignUp(
string pageUrl, SolveResult solved, string email, Uri? proxy, CancellationToken ct)
{
var handler = new HttpClientHandler { CookieContainer = new CookieContainer() };
if (proxy is not null)
{
var address = new UriBuilder(proxy.Scheme, proxy.Host, proxy.Port).Uri;
var login = proxy.UserInfo.Split(':', 2).Select(Uri.UnescapeDataString).ToArray();
handler.Proxy = new WebProxy(address)
{
Credentials = login.Length == 2 ? new NetworkCredential(login[0], login[1]) : null,
};
}
using var http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
// The User-Agent the token was earned with, on the page load and on the post.
if (solved.UserAgent is not null)
http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", solved.UserAgent);
using (await http.GetAsync(pageUrl, ct)) { } // the page's own cookies land in the jar
var origin = new Uri(pageUrl).GetLeftPart(UriPartial.Authority);
using var post = new HttpRequestMessage(HttpMethod.Post, pageUrl)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = email, // your form's own fields go here
["h-captcha-response"] = solved.Token!, // always set on an hCaptcha result
["g-recaptcha-response"] = solved.Token!, // the widget fills both
}),
};
post.Headers.TryAddWithoutValidation("Referer", pageUrl); // HttpClient sends none
post.Headers.TryAddWithoutValidation("Origin", origin);
using var response = await http.SendAsync(post, ct);
return (response.StatusCode, await response.Content.ReadAsStringAsync(ct));
}
}第 4 步:运行完整的 C# hCaptcha 循环
最后一个代码块是 Main 方法。请编辑 PageUrl 和 SiteKey 常量(按粘贴的原样,它请求的是 example.com,那里没有小组件),然后在项目文件夹里运行 dotnet run。它在一个 CancellationTokenSource 之下最多进行三次尝试,每次尝试内部都有 SDK 自己的 300 秒等待。按我对 CapSkip 4.1.0 源码的理解,在等待 Max. Threads 名额最多 250 秒之后,一个任务在 CapSkip 判定其无法识别之前最多可以运行 300 秒,所以负载高时请调大 recaptchaTimeout。
按文档所说,ERROR_CAPTCHA_UNSOLVABLE 是唯一一个立刻新建任务可能有帮助的错误码。超出速率限制的密钥会收到 ERROR_NO_SLOT_AVAILABLE,暂停 5 秒后重试。其他所有错误码都会结束运行,ValidationException 也一样,因为同样的参数会再次被拒绝。返回 400 或更高状态码的站点会得到新任务和新 token。200 并不总是代表成功,因为很多站点把错误放在页面里,所以请把状态码判断换成只有成功时才会出现的内容。表单提交的 30 秒 HttpClient.Timeout 到期时,最后一个 catch 会报告它。
static class Program
{
const string PageUrl = "https://example.com/signup"; // the page that shows the widget
const string SiteKey = "YOUR_SITEKEY"; // the data-sitekey of its h-captcha div
static readonly Uri? ProxyUri = null; // for example new Uri("http://user:[email protected]:8080")
// The reply body is the exception message, JSON on a poll and bare text on a submit.
static string ErrorCode(ApiException ex)
{
var found = Regex.Match(ex.Message, "ERROR_[A-Z_]+");
return found.Success ? found.Value : ex.Message;
}
static async Task<int> Main()
{
// One budget for every attempt. Cancelling it stops a wait or a request at once.
using var budget = new CancellationTokenSource(TimeSpan.FromMinutes(6));
try
{
for (var attempt = 1; attempt <= 3; attempt++)
{
SolveResult solved;
try
{
solved = await Solve.Run(SiteKey, PageUrl, invisible: false, rqData: null, ProxyUri, budget.Token);
}
catch (ApiException ex) when (ErrorCode(ex) is "ERROR_CAPTCHA_UNSOLVABLE" or "ERROR_NO_SLOT_AVAILABLE")
{
Console.WriteLine($"attempt {attempt}: {ErrorCode(ex)}, new task");
// The second code means the key is over its rate limit, so pause first.
if (ErrorCode(ex) == "ERROR_NO_SLOT_AVAILABLE") await Task.Delay(TimeSpan.FromSeconds(5), budget.Token);
continue;
}
var (status, body) = await Site.SignUp(PageUrl, solved, "[email protected]", ProxyUri, budget.Token);
if ((int)status < 400)
{
Console.WriteLine($"{(int)status} {body[..Math.Min(40, body.Length)]}");
return 0;
}
Console.WriteLine($"attempt {attempt}: the site answered {(int)status}, new token");
}
Console.WriteLine("no accepted form after 3 attempts");
}
// A cancelled submit surfaces as a NetworkException, a cancelled poll as a
// TaskCanceledException, so test the budget itself.
catch (Exception) when (budget.IsCancellationRequested) { Console.WriteLine("the 6 minute budget ran out"); }
catch (ApiException ex) { Console.WriteLine($"CapSkip answered {ErrorCode(ex)}"); }
catch (SolverTimeout ex) { Console.WriteLine($"no token in time: {ex.Message}"); }
catch (CapSkipError ex) { Console.WriteLine($"{ex.GetType().Name}: {ex.Message}"); }
catch (HttpRequestException ex) { Console.WriteLine($"request failed: {ex.Message}"); }
catch (TaskCanceledException) { Console.WriteLine("the site did not answer in 30 seconds"); } // HttpClient.Timeout
return 1;
}
}针对这些模拟服务器(它们无法证明真实站点的任何情况),每一行在 net10.0 和 net8.0 上的结果都相同。
| 模拟设置 | 程序的表现 |
|---|---|
| token 在第 7 次轮询时就绪,使用 SDK 默认值 | 一个任务,轮询出现在提交后 0、0.25、0.75、1.75、3.75、7.75 和 12.75 秒的 0.15 秒误差范围内,然后返回 200 |
| 两次拒绝(ERROR_CAPTCHA_UNSOLVABLE,或带暂停的 ERROR_NO_SLOT_AVAILABLE),然后成功;或者每次都被拒绝 | 第三个任务返回 200;或者三行 attempt 输出、退出码 1,且没有向站点发出请求 |
| 站点用 403 拒绝了第一次表单提交 | 新任务和新 token,然后返回 200 |
| 空 sitekey | 一行 ValidationException 输出和退出码 1,模拟服务器没有收到任务 |
| ERROR_ZERO_BALANCE、ERROR_PROXY_CONNECT_REFUSED 或 ERROR_RECAPTCHA_INVALID_SITEKEY | 每个都在一个任务之后终止了运行 |
| 隐形调用、Enterprise 调用、带登录信息或不带登录信息的代理 | in.php 看到了 invisible=1、data 值,或者 proxytype HTTP 加上地址,站点请求则通过代理模拟服务器到达 |
| 构造函数中带 apiKey,密钥验证开启 | 密钥出现在提交和每一次轮询中。默认密钥得到了 ERROR_KEY_DOES_NOT_EXIST |
| recaptchaTimeout 为 22,模拟服务器始终不回答 | TimeoutException,调用后 22.9 秒 |
| 预算为 2 秒,发生在轮询等待期间或 in.php 挂起时 | 两次都是在 2.0 秒之后出现预算消息 |
| CapSkip 或站点处没有程序在监听;站点页面始终不回答 | NetworkException;request failed;30 秒后出现 30 秒的那条消息 |
| 同时三次注册 | 三个任务,三个会话,提交时三个不同的 User-Agent,三个 200 响应 |
还有两个代码片段涵盖其余情况。第一个把调用换成隐形或 Enterprise 小组件的版本。第二个同时注册多个账号:把 Main 中外层 try 的花括号之间的行替换成它,并保留 catch 那几行。每次注册都会构建自己的 handler,而模拟服务器给每个任务各自的 User-Agent,所以一旦混淆,提交就会失败。这个片段不传 rqData,所以对于 Enterprise 站点,请在 lambda 内部读取一个新值。
// Snippet, not part of Program.cs. In Main, replace the Solve.Run line with one of these. // An invisible widget (data-size="invisible"): solved = await Solve.Run(SiteKey, PageUrl, invisible: true, rqData: null, ProxyUri, budget.Token); // An Enterprise widget. Read a fresh rqdata value from the page before every attempt, // because it is single use. The literal below stands in for that code. solved = await Solve.Run(SiteKey, PageUrl, invisible: false, rqData: "RQDATA_FROM_THE_PAGE", ProxyUri, budget.Token);
// Snippet, not part of Program.cs. In Main, replace the lines between the braces of the outer try
// with these, and keep the catch lines. Three accounts sign up at once on one SDK client.
var emails = new[] { "[email protected]", "[email protected]", "[email protected]" };
var codes = await Task.WhenAll(emails.Select(async email =>
{
var solved = await Solve.Run(SiteKey, PageUrl, false, null, ProxyUri, budget.Token);
var (status, _) = await Site.SignUp(PageUrl, solved, email, ProxyUri, budget.Token);
return (int)status;
}));
Console.WriteLine(string.Join(" ", codes));
return codes.All(c => c < 400) ? 0 : 1;常见错误
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| ValidationException | 发送之前就被拒绝:空的 sitekey、不是绝对 http 或 https 的页面地址、错误的 invisible 值,或者 hCaptcha API 不接受的参数,例如 domain | 修正参数。重试没有用 |
| ERROR_RECAPTCHA_INVALID_SITEKEY | 该页面不接受这个 sitekey | 重新复制 data-sitekey,并检查页面地址 |
| ERROR_CAPTCHA_UNSOLVABLE | CapSkip 没有通过小组件 | 循环会提交新任务,最多三次 |
| ERROR_PROXY_FORMAT 或 ERROR_PROXY_CONNECT_REFUSED | CapSkip 读不懂代理值,或者无法通过它到达目标(第二种情况我只做了模拟) | 传入带协议、主机和端口的 Uri,并单独测试这个代理 |
| ERROR_WRONG_METHOD | 早于 4.0.0 的 CapSkip 可能不认识 hCaptcha 方法(未测试) | 把 CapSkip 更新到 4.0.0 或更高版本 |
| ERROR_NO_SLOT_AVAILABLE、ERROR_ZERO_BALANCE 或 ERROR_IP_NOT_ALLOWED | 只在 API Key Validation 开启时出现。按我对 4.1.0 源码的理解:密钥超出了速率限制、额度用完,或者一个限定单设备的密钥正在别处使用 | 对第一个,循环会暂停并重试。其余几个要在 CapSkip 中更改密钥的设置 |
| ERROR_WRONG_USER_KEY 或 ERROR_KEY_DOES_NOT_EXIST | API Key Validation 已开启,而你构造函数调用中的 apiKey 缺失或有误 | 在 CapSkipClient 那一行传入有效的 apiKey |
| TimeoutException | 在 recaptchaTimeout 之内没有 token 到达。空回复算作未就绪,所以 SDK 会继续轮询 | 在 CapSkip 的 Monitor 中查看该任务,然后调大 recaptchaTimeout |
| 调用开始时出现 NetworkException | 该主机和端口上没有程序应答。内部异常是 HttpRequestException | 运行 CapSkip,然后把它在连接设置里的主机和端口复制到你的构造函数调用中 |
| Error CS0104:引用不明确 | System 和 CapSkip 里都有 TimeoutException。一旦你导入 System.ComponentModel.DataAnnotations,ValidationException 也会同样冲突 | 像第一个代码块那样给 SDK 的类起别名,或者写出它的完整名称 |
| 看起来有效的 token 被拒绝 | 文档列出的原因有:token 被重复使用、早于约两分钟、User-Agent 与结果中的不同。我的模拟服务器还会拒绝缺失的 User-Agent、Referer、Origin 或会话 cookie。407 表示代理需要登录 | 每个 token 只提交一次,拿到后立刻从 Site.SignUp 提交 |
在服务器上使用 CapSkip
在 Server 模式下,CapSkip 监听你的网络地址或公网 IP,所以同样的 C# hCaptcha 代码可以在另一台 PC、Windows VPS 或托管平台上运行。Server 模式要求 CapSkip 以管理员身份运行,设置指南建议使用静态公网 IP。把 CapSkipClient 那一行里的主机改成服务器的地址,其余都不用改。
Server 模式启动时,CapSkip 会向所有地址开放它的端口,所以要把访问限制在你预期的机器上,请为其他地址添加一条 Windows Firewall 阻止规则,并开启 API Key Validation,在 CapSkipClient 那一行里填写 apiKey。现在识别在服务器上运行,所以 hCaptcha 看到的是那台机器的网络。
常见问题
怎样在 C# 中识别隐形或 Enterprise 的 hCaptcha?
用同样的 HCaptchaAsync 调用。对于 data-size 为 invisible 的小组件,给 invisible bool 传 true。对于设置了 rqdata 的 Enterprise 站点,把值放在键 data 下传入,并且每个任务都读取一个新的,因为文档说站点为一次挑战签发一个。第 4 步的第一个代码片段同时展示了这两种情况。
我可以用 IHttpClientFactory 注册这个 SDK 吗?
没有什么需要注册。这个包用它自己的一个静态 HttpClient 向 CapSkip 发起调用。也不要把 Site.SignUp 交给 factory:微软的指南警告说,池化的 handler 会共享它们的 CookieContainer。
为什么站点会拒绝 CapSkip 给我的 token?
严格的站点可能会拒绝 hCaptcha 判为有风险的 token,即使小组件已经通过,Enterprise 站点尤其常见。文档建议使用 hCaptcha 设置中的 High score mode。它需要 CapSkip 所在的 PC 上装有 Google Chrome,按我对 4.1.0 源码的理解,它默认是开启的。我没有严格的站点可以测试。
在 C# 中识别 hCaptcha,归结起来就是一次 HCaptchaAsync 调用、一个只重试值得重试的错误码的循环,以及一个带着正确的 cookie 和 User-Agent 从 handler 发出的表单提交。CapSkip 是一个本地 验证码绕过 工具,采用统一授权定价,所以再试一次只花费所需的时间。 C# 验证码识别页面 介绍了 .NET 中的其他验证码类型,而 hCaptcha 演示 展示了小组件被通过的过程。
