{"id":24879,"date":"2026-08-05T11:24:13","date_gmt":"2026-08-05T11:24:13","guid":{"rendered":"https:\/\/capskip.com\/?p=24879"},"modified":"2026-08-05T11:24:13","modified_gmt":"2026-08-05T11:24:13","slug":"geetest-v3-csharp","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/geetest-v3-csharp\/","title":{"rendered":"\u5982\u4f55\u7528 C# \u8bc6\u522b\u6781\u9a8c v3"},"content":{"rendered":"<p>GeeTest breaks the mental model most CAPTCHA code is built on. There is no single token to drop into a form field. A solve returns <strong>three<\/strong> values that have to be posted back together, and the challenge you started from expires in about a minute. Get either part wrong and the request fails with no useful error.<\/p>\n<p>Here is the whole flow in C#, including the parts that are easy to get wrong.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Two inputs, and only one of them is stable<\/h2>\n<p>GeeTest v3 identifies a site with <code>gt<\/code> and a single attempt with <code>challenge<\/code>. They behave completely differently.<\/p>\n<table>\n<thead>\n<tr>\n<th>Value<\/th>\n<th>Lifetime<\/th>\n<th>Where it comes from<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>gt<\/code><\/td>\n<td>Static for the site. Safe to hard-code or cache<\/td>\n<td>The site&#8217;s GeeTest init response<\/td>\n<\/tr>\n<tr>\n<td><code>challenge<\/code><\/td>\n<td><strong>Single use, expires in roughly 60 seconds<\/strong><\/td>\n<td>The same init response, fresh every time<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That expiry is the single most common cause of GeeTest failures. If you fetch a pair, queue the job, and solve it thirty seconds later behind other work, the challenge may already be dead. Fetch it immediately before solving, never from a cache.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Setup<\/h2>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Targets .NET Standard 2.0, so Framework 4.6.1+ and .NET 6-9 all work.\ndotnet add package CapSkip<\/pre>\n<p>CapSkip runs on your own machine, so start the app first and point the client at the port from its settings:<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">using CapSkip;\n\nvar solver = new CapSkipClient(\n    apiKey: &quot;capskip&quot;,        \/\/ any string when key validation is off\n    host: &quot;127.0.0.1&quot;,\n    port: 8080,\n    recaptchaTimeout: 300);   \/\/ seconds, and this one covers GeeTest too<\/pre>\n<p>Note that GeeTest uses <code>recaptchaTimeout<\/code>, not <code>defaultTimeout<\/code>. The latter only applies to image CAPTCHAs.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Solving it<\/h2>\n<p>Three positional arguments, in this order:<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">var result = await solver.GeetestAsync(\n    &quot;81388ea1fc187e0c335c0a8907ff2625&quot;,   \/\/ gt, static per site\n    &quot;7cf6a8b1a2c34d5e6f7089abcdef0123&quot;,   \/\/ challenge, fetched seconds ago\n    &quot;https:\/\/example.com\/login&quot;);\n\nConsole.WriteLine(result.Challenge);\nConsole.WriteLine(result.Validate);\nConsole.WriteLine(result.Seccode);<\/pre>\n<p>Those three properties are the answer. <code>result.Code<\/code> is also populated, but for GeeTest it holds the raw JSON string rather than a usable token, so reaching for <code>Code<\/code> out of habit is a mistake. On <code>SolveResult<\/code>, <code>Challenge<\/code>, <code>Validate<\/code> and <code>Seccode<\/code> are GeeTest-only and null for every other type.<\/p>\n<p>Worth knowing: the <code>challenge<\/code> that comes back is not always the one you sent in. Use the returned value, not your input.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Posting the answer back<\/h2>\n<p>Submit all three exactly as the site&#8217;s own front end would. Most GeeTest v3 integrations use these field names, though a site can rename them, so check the real form before assuming:<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">using System.Net.Http;\nusing System.Collections.Generic;\n\nusing var http = new HttpClient();\n\nvar form = new FormUrlEncodedContent(new[]\n{\n    new KeyValuePair&lt;string, string&gt;(&quot;geetest_challenge&quot;, result.Challenge),\n    new KeyValuePair&lt;string, string&gt;(&quot;geetest_validate&quot;,  result.Validate),\n    new KeyValuePair&lt;string, string&gt;(&quot;geetest_seccode&quot;,   result.Seccode),\n    new KeyValuePair&lt;string, string&gt;(&quot;username&quot;, &quot;...&quot;),\n    new KeyValuePair&lt;string, string&gt;(&quot;password&quot;, &quot;...&quot;),\n});\n\nvar response = await http.PostAsync(&quot;https:\/\/example.com\/login&quot;, form);<\/pre>\n<p>Sending two of the three, or pairing a fresh <code>validate<\/code> with a stale <code>challenge<\/code>, gets rejected the same way a wrong answer would.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The whole flow, in order<\/h2>\n<p>Fetching the pair and solving have to sit next to each other. This shape keeps them there:<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">using System;\nusing System.Net.Http;\nusing System.Text.Json;\nusing CapSkip;\n\n\/\/ 1. Fetch a fresh gt\/challenge pair from the site's own init endpoint.\nusing var http = new HttpClient();\nvar initJson = await http.GetStringAsync(\n    &quot;https:\/\/example.com\/geetest\/init?t=&quot; + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());\n\nusing var doc = JsonDocument.Parse(initJson);\nvar gt        = doc.RootElement.GetProperty(&quot;gt&quot;).GetString();\nvar challenge = doc.RootElement.GetProperty(&quot;challenge&quot;).GetString();\n\n\/\/ 2. Solve immediately. Do not queue this or await anything slow in between.\nvar result = await solver.GeetestAsync(gt, challenge, &quot;https:\/\/example.com\/login&quot;);\n\n\/\/ 3. Post all three values together.\nConsole.WriteLine($&quot;{result.Challenge} {result.Validate} {result.Seccode}&quot;);<\/pre>\n<p>The cache-busting timestamp on the init call matters more than it looks. GeeTest init endpoints are frequently cached by intermediaries, and a cached response hands you a challenge that was already consumed.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">When it fails<\/h2>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">using System;\nusing CapSkip;\n\ntry\n{\n    var result = await solver.GeetestAsync(gt, challenge, pageUrl);\n}\ncatch (CapSkip.ValidationException) { \/* missing gt or challenge *\/ }\ncatch (NetworkException)            { \/* CapSkip is not running *\/ }\ncatch (ApiException)                { \/* API error, often a dead challenge *\/ }\ncatch (CapSkip.TimeoutException)    { \/* exceeded recaptchaTimeout *\/ }\ncatch (CapSkipError)                { \/* anything else from the SDK *\/ }<\/pre>\n<p>Qualify <code>ValidationException<\/code> and <code>TimeoutException<\/code> with the <code>CapSkip<\/code> namespace. Both names also exist in <code>System<\/code>, and with both namespaces imported an unqualified catch binds to the <code>System<\/code> type and silently never fires. Catching the base <code>CapSkipError<\/code> sidesteps the problem entirely.<\/p>\n<p>In practice most GeeTest failures surface as <code>ApiException<\/code> and mean the challenge died before the solve finished. The fix is fetching later, not retrying with the same values.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Proxies and concurrency<\/h2>\n<p>GeeTest is one of the three types that accept a proxy, alongside reCAPTCHA and Turnstile. Image CAPTCHAs do not, because they never touch the target site.<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">using System.Collections.Generic;\n\nvar result = await solver.GeetestAsync(gt, challenge, pageUrl,\n    new Dictionary&lt;string, object?&gt;\n    {\n        [&quot;proxy&quot;] = new Proxy(&quot;HTTPS&quot;, &quot;user:pass@1.2.3.4:3128&quot;),\n    });<\/pre>\n<p>Running several in parallel works, but each one needs its own freshly fetched pair. Do not fetch a batch of challenges up front and then solve them together, because the last ones will have expired before their turn.<\/p>\n<pre data-enlighter-language=\"csharp\" class=\"EnlighterJSRAW\">\/\/ Correct: fetch and solve inside the same task.\nvar tasks = urls.Select(async url =&gt;\n{\n    var (gt, challenge) = await FetchPairAsync(url);\n    return await solver.GeetestAsync(gt, challenge, url);\n});\n\nvar results = await Task.WhenAll(tasks);<\/pre>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Frequently asked questions<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Why is result.Code not a usable token?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because GeeTest&#8217;s answer is three values, not one. <code>Code<\/code> keeps the raw JSON string for completeness, while the SDK expands the useful parts into <code>Challenge<\/code>, <code>Validate<\/code> and <code>Seccode<\/code>. Use those three.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I cache the challenge to save a request?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. It is single use and expires in about a minute. Caching it is the most common reason GeeTest integrations work in testing and fail under load, because queueing delay pushes the solve past the expiry window. The <code>gt<\/code> value is safe to cache.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does this work for GeeTest v4?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The <code>GeetestAsync<\/code> method targets v3, the slide-puzzle version built around the <code>gt<\/code> and <code>challenge<\/code> pair. v4 changed the parameter model, so check the current <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a> for what is supported before assuming the same call works.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Do I need a proxy?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only when the site is geo-sensitive or already treats your address as suspicious. Solve and submit from the same network path when you do use one, otherwise the mismatch can itself trigger a re-challenge.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Fetch <code>gt<\/code> and <code>challenge<\/code> immediately before solving, call <code>GeetestAsync<\/code> with both plus the page URL, then post <code>Challenge<\/code>, <code>Validate<\/code> and <code>Seccode<\/code> back together. Treat the challenge as perishable, use the returned challenge rather than your input, and catch <code>CapSkipError<\/code> to avoid the namespace collision.<\/p>\n<p>Method signatures for the other languages are on the <a href=\"https:\/\/capskip.com\/geetest-solver\/\">GeeTest solver<\/a> page, the full .NET surface is on the <a href=\"https:\/\/capskip.com\/csharp-captcha-solver\/\">C# CAPTCHA solver<\/a> page, and you can try a live puzzle on our <a href=\"https:\/\/capskip.com\/captcha-demo\/geetest-v3\/\">GeeTest v3 demo<\/a>. CapSkip handles <a href=\"https:\/\/capskip.com\/\">captcha bypass<\/a> locally, so solve volume costs nothing per request.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u6781\u9a8c\u4e0d\u4f1a\u53ea\u4ea4\u56de\u4e00\u4e2a token\u3002\u5b83\u8fd4\u56de\u4e09\u4e2a\u5fc5\u987b\u4e00\u8d77\u63d0\u4ea4\u7684\u503c\uff0c\u800c\u4e14\u4f60\u6240\u4f9d\u636e\u7684\u6311\u6218\u7ea6\u4e00\u5206\u949f\u540e\u8fc7\u671f\u3002<\/p>","protected":false},"author":1,"featured_media":24878,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve GeeTest v3 in C# (.NET) | CapSkip","rank_math_description":"To solve GeeTest in C# you need a fresh challenge, then three values posted back together. Here is the .NET code, and the 60-second expiry that breaks it.","rank_math_focus_keyword":"solve geetest in c#","footnotes":""},"categories":[70],"tags":[],"class_list":["post-24879","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24879","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=24879"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24879\/revisions"}],"predecessor-version":[{"id":24880,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24879\/revisions\/24880"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/24878"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=24879"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=24879"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=24879"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}