如何用 C# 识别极验 v3

solve geetest in c# - How to Solve GeeTest v3 in C# and Post the Result Back

极验打破了大多数验证码代码所基于的心智模型。没有单一 token 可以放入表单字段。一次识别返回 三个 必须一起回传的值,而且你所依据的挑战约一分钟后过期。任一部分出错,请求就会失败,且没有有用的错误信息。

下面是 C# 中的完整流程,包括那些容易出错的部分。

两个输入,其中只有一个是稳定的

极验 v3 用 gt 识别一个站点,用 challenge识别单次尝试。它们的行为完全不同。

有效期来源
gt对站点是静态的。可安全地硬编码或缓存站点的极验初始化响应
challenge一次性,约 60 秒后过期同一个初始化响应,每次都是新的

该过期时间是极验失败最常见的单一原因。如果你获取一对参数、把任务排队,然后在其他工作之后三十秒才识别,挑战可能已经失效。请在识别前立即获取它,绝不要从缓存获取。

设置

# 目标为 .NET Standard 2.0,因此 Framework 4.6.1+ 与 .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, and this one covers GeeTest too

请注意,极验使用 recaptchaTimeout,而不是 defaultTimeout。后者仅适用于图片验证码。

识别它

三个位置参数,按此顺序:

var result = await solver.GeetestAsync(
    "81388ea1fc187e0c335c0a8907ff2625",   // gt, static per site
    "7cf6a8b1a2c34d5e6f7089abcdef0123",   // challenge, fetched seconds ago
    "https://example.com/login");

Console.WriteLine(result.Challenge);
Console.WriteLine(result.Validate);
Console.WriteLine(result.Seccode);

那三个属性就是答案。 result.Code 也会被填充,但对极验来说它保存的是原始 JSON 字符串而非可用的 token,因此出于习惯去用 代码 是个错误。在 SolveResult, Challenge, ValidateSeccode 上,它们是极验专有的,对其他所有类型都为 null。

值得知道的是:返回的 challenge 并不总是你传入的那个。请使用返回值,而不是你的输入。

回传答案

将三个值完全按照站点自己前端的方式提交。大多数极验 v3 集成使用以下字段名,不过站点可能重命名它们,因此在假设之前请检查真实表单:

using System.Net.Http;
using System.Collections.Generic;

using var http = new HttpClient();

var form = new FormUrlEncodedContent(new[]
{
    new KeyValuePair<string, string>("geetest_challenge", result.Challenge),
    new KeyValuePair<string, string>("geetest_validate",  result.Validate),
    new KeyValuePair<string, string>("geetest_seccode",   result.Seccode),
    new KeyValuePair<string, string>("username", "..."),
    new KeyValuePair<string, string>("password", "..."),
});

var response = await http.PostAsync("https://example.com/login", form);

只发送三个中的两个,或把新的 validate 与过期的 challenge配对,都会像错误答案一样被拒绝。

完整流程,按顺序

获取参数对和识别必须紧挨在一起。这种结构让它们保持相邻:

using System;
using System.Net.Http;
using System.Text.Json;
using CapSkip;

// 1. Fetch a fresh gt/challenge pair from the site's own init endpoint.
using var http = new HttpClient();
var initJson = await http.GetStringAsync(
    "https://example.com/geetest/init?t=" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());

using var doc = JsonDocument.Parse(initJson);
var gt        = doc.RootElement.GetProperty("gt").GetString();
var challenge = doc.RootElement.GetProperty("challenge").GetString();

// 2. Solve immediately. Do not queue this or await anything slow in between.
var result = await solver.GeetestAsync(gt, challenge, "https://example.com/login");

// 3. Post all three values together.
Console.WriteLine($"{result.Challenge} {result.Validate} {result.Seccode}");

初始化调用上的缓存破坏时间戳比看起来更重要。极验初始化端点经常被中间层缓存,而缓存的响应会给你一个已被消费的挑战。

当它失败时

using System;
using CapSkip;

try
{
    var result = await solver.GeetestAsync(gt, challenge, pageUrl);
}
catch (CapSkip.ValidationException) { /* missing gt or challenge */ }
catch (NetworkException)            { /* CapSkip is not running */ }
catch (ApiException)                { /* API error, often a dead challenge */ }
catch (CapSkip.TimeoutException)    { /* exceeded recaptchaTimeout */ }
catch (CapSkipError)                { /* anything else from the SDK */ }

Qualify ValidationExceptionTimeoutException 连同 CapSkip 命名空间。这两个名称在 System中也存在,当两个命名空间都被导入时,未限定的 catch 会绑定到 System 类型并静默地永不触发。捕获基类 CapSkipError 可完全规避这个问题。

实际上,大多数极验失败会以 ApiException 形式出现,意味着挑战在识别完成前就失效了。修复方法是稍后再获取,而不是用相同的值重试。

代理与并发

极验是接受代理的三种类型之一,另外两种是 reCAPTCHA 和 Turnstile。图片验证码不接受,因为它们从不接触目标站点。

using System.Collections.Generic;

var result = await solver.GeetestAsync(gt, challenge, pageUrl,
    new Dictionary<string, object?>
    {
        ["proxy"] = new Proxy("HTTPS", "user:[email protected]:3128"),
    });

并行运行多个是可行的,但每个都需要自己新获取的参数对。不要预先获取一批挑战再一起识别,因为最后那些会在轮到它们之前就已过期。

// Correct: fetch and solve inside the same task.
var tasks = urls.Select(async url =>
{
    var (gt, challenge) = await FetchPairAsync(url);
    return await solver.GeetestAsync(gt, challenge, url);
});

var results = await Task.WhenAll(tasks);

常见问题

为什么 result.Code 不是可用的 token?

因为极验的答案是三个值,而非一个。 代码 保留原始 JSON 字符串以求完整,而 SDK 将有用的部分展开到 Challenge, ValidateSeccode中。使用那三个。

我可以缓存挑战以省下一次请求吗?

不可以。它是一次性的,约一分钟后过期。缓存它是极验集成在测试中有效、在负载下失败的最常见原因,因为排队延迟会把识别推过过期窗口。 gt 值可以安全缓存。

这适用于极验 v4 吗?

加载的 GeetestAsync 方法针对 v3,即围绕 gtchallenge 参数对构建的滑块拼图版本。v4 改变了参数模型,因此在假设同一调用可用之前,请查看当前的 API 文档 以了解支持内容。

我需要代理吗?

仅当站点对地理位置敏感,或已将你的地址视为可疑时。使用代理时,请从同一网络路径识别并提交,否则不匹配本身可能触发再次挑战。

小结

获取 gtchallenge ,在识别前立即获取,用两者加上页面 URL 调用 GeetestAsync ,然后把 Challenge, ValidateSeccode 一起回传。将挑战视为易腐的,使用返回的 challenge 而非你的输入,并捕获 CapSkipError 以避免命名空间冲突。

其他语言的方法签名见 GeeTest 识别工具 页面,完整的 .NET 用法见 C# 验证码识别 页面,你还可以在我们的 GeeTest v3 演示上尝试一个实时拼图。CapSkip 在本地处理 验证码绕过 ,因此识别量不产生任何按次费用。