{"id":24987,"date":"2026-08-06T10:27:08","date_gmt":"2026-08-06T10:27:08","guid":{"rendered":"https:\/\/capskip.com\/?p=24987"},"modified":"2026-08-06T10:27:08","modified_gmt":"2026-08-06T10:27:08","slug":"geetest-v3-php","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/geetest-v3-php\/","title":{"rendered":"\u5982\u4f55\u7528 PHP \u8bc6\u522b\u6781\u9a8c v3 \u5e76\u56de\u4f20\u7ed3\u679c"},"content":{"rendered":"<p>Most CAPTCHA code assumes one token in and one token out. GeeTest does not fit that. A solve returns <strong>three<\/strong> values that must be posted together, and the challenge you fed in expires roughly sixty seconds after it was issued. In PHP, where work often gets pushed onto a queue, that expiry causes a very specific and very confusing class of bug.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Two inputs that behave differently<\/h2>\n<table>\n<thead>\n<tr>\n<th>Value<\/th>\n<th>Lifetime<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>gt<\/code><\/td>\n<td>Static per site. Safe to cache<\/td>\n<\/tr>\n<tr>\n<td><code>challenge<\/code><\/td>\n<td><strong>Single use, dead in about 60 seconds<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Both come from the site&#8217;s own GeeTest init endpoint. The <code>gt<\/code> identifies the site; the <code>challenge<\/code> is per attempt and perishes quickly.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Setup<\/h2>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># PHP 8.0+, with the curl and json extensions.\ncomposer require capskip\/capskip<\/pre>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">use CapSkip\\CapSkip;\n\n$solver = new CapSkip([\n    'host' =&gt; '127.0.0.1',\n    'port' =&gt; 8080,\n    'recaptchaTimeout' =&gt; 300,   \/\/ GeeTest uses this, not defaultTimeout\n]);<\/pre>\n<p><code>defaultTimeout<\/code> only governs image CAPTCHAs. Everything interactive, GeeTest included, uses <code>recaptchaTimeout<\/code>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Solving<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;geetest(\n    '81388ea1fc187e0c335c0a8907ff2625',   \/\/ gt, static per site\n    '7cf6a8b1a2c34d5e6f7089abcdef0123',   \/\/ challenge, fetched seconds ago\n    'https:\/\/example.com\/login'\n);\n\necho $result['challenge'];\necho $result['validate'];\necho $result['seccode'];<\/pre>\n<p>Those three keys are the answer. <code>$result['code']<\/code> is populated as well, but for GeeTest it holds the raw JSON string rather than anything submittable, so grabbing it out of habit produces a puzzling failure.<\/p>\n<p>Use the <code>challenge<\/code> that comes back, not the one you sent in. They are not always the same.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The whole flow<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">use CapSkip\\CapSkip;\n\n$solver = new CapSkip();\n$login = 'https:\/\/example.com\/login';\n\n\/\/ 1. Fresh pair, cache-busted. Cached init responses return spent challenges.\n$init = json_decode(file_get_contents(\n    'https:\/\/example.com\/geetest\/init?t=' . (int) (microtime(true) * 1000)\n), true);\n\n\/\/ 2. Solve immediately. Nothing slow between here and step 1.\n$result = $solver-&gt;geetest($init['gt'], $init['challenge'], $login);\n\n\/\/ 3. Post all three together with the real form fields.\n$ch = curl_init($login);\ncurl_setopt_array($ch, [\n    CURLOPT_POST =&gt; true,\n    CURLOPT_RETURNTRANSFER =&gt; true,\n    CURLOPT_POSTFIELDS =&gt; http_build_query([\n        'geetest_challenge' =&gt; $result['challenge'],\n        'geetest_validate' =&gt; $result['validate'],\n        'geetest_seccode' =&gt; $result['seccode'],\n        'username' =&gt; '...',\n        'password' =&gt; '...',\n    ]),\n]);\n$response = curl_exec($ch);\ncurl_close($ch);<\/pre>\n<p>The cache-busting parameter is doing real work. GeeTest init endpoints are often cached by a CDN or reverse proxy, and a cached response hands you a challenge somebody already consumed.<\/p>\n<p>Those three field names are the usual GeeTest v3 convention, but a site can rename them, so check the real form first.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The queue trap, which is worse in PHP<\/h2>\n<p>PHP applications lean heavily on job queues, and GeeTest interacts badly with the obvious design. Dispatching a job that carries a <code>gt<\/code> and <code>challenge<\/code> looks reasonable and fails as soon as the queue has any backlog:<\/p>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">\/\/ WRONG: the challenge expires while the job waits to be picked up.\n$init = fetchGeetestPair();\ndispatch(new SolveCaptchaJob($init['gt'], $init['challenge'], $url));<\/pre>\n<p>Fetch the pair inside the worker instead, so the challenge is seconds old when it is used:<\/p>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">\/\/ RIGHT: the job fetches its own pair at the moment it runs.\nclass SolveCaptchaJob\n{\n    public function handle(CapSkip $solver): array\n    {\n        $init = fetchGeetestPair();   \/\/ called inside the worker\n        return $solver-&gt;geetest($init['gt'], $init['challenge'], $this-&gt;url);\n    }\n}<\/pre>\n<p>PHP executes synchronously and <code>AsyncCapSkip<\/code> is only an alias here, so parallelism comes from running more workers rather than from the SDK.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Errors<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">use CapSkip\\Exceptions\\ValidationException;\nuse CapSkip\\Exceptions\\NetworkException;\nuse CapSkip\\Exceptions\\ApiException;\nuse CapSkip\\Exceptions\\TimeoutException;\n\ntry {\n    $result = $solver-&gt;geetest($gt, $challenge, $pageUrl);\n} catch (ValidationException $e) {\n    \/\/ missing gt or challenge\n} catch (NetworkException $e) {\n    \/\/ CapSkip is not running\n} catch (ApiException $e) {\n    \/\/ usually a challenge that expired before the solve finished\n} catch (TimeoutException $e) {\n    \/\/ exceeded recaptchaTimeout\n}<\/pre>\n<p>Most GeeTest failures arrive as <code>ApiException<\/code> and mean the challenge died. Retrying with the same pair never succeeds, because a spent challenge does not become valid again.<\/p>\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[&#8216;code&#8217;] not usable?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because the answer is three values rather than one. <code>code<\/code> keeps the raw JSON for completeness while the SDK expands the useful parts into <code>challenge<\/code>, <code>validate<\/code> and <code>seccode<\/code>. Post 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 store the challenge in the database?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. It is single use and expires in about a minute, so anything that persists it will hand a dead value to whatever reads it back. Cache the <code>gt<\/code> if you like; never the challenge.<\/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 cover GeeTest v4?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The <code>geetest<\/code> method targets v3, the slide puzzle built on a <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> before assuming the same call applies.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Fetch a cache-busted pair, solve immediately, then post <code>challenge<\/code>, <code>validate<\/code> and <code>seccode<\/code> together. Use the returned challenge, cache only the <code>gt<\/code>, and fetch inside the worker rather than passing a challenge through a queue.<\/p>\n<p>Other languages are on the <a href=\"https:\/\/capskip.com\/geetest-solver\/\">GeeTest solver<\/a> page, the wider PHP surface on the <a href=\"https:\/\/capskip.com\/php-captcha-solver\/\">PHP CAPTCHA solver<\/a> page, and there is 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 volume costs nothing per solve.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u6781\u9a8c\u8fd4\u56de\u7684\u662f\u4e09\u4e2a\u503c\u800c\u975e\u4e00\u4e2a token\uff0c\u4e14\u6311\u6218\u7ea6\u4e00\u5206\u949f\u540e\u8fc7\u671f\u3002\u4e0b\u9762\u662f PHP \u6d41\u7a0b\uff0c\u4ee5\u53ca\u4e3a\u4ec0\u4e48\u4efb\u52a1\u961f\u5217\u4f1a\u7834\u574f\u5b83\u3002<\/p>","protected":false},"author":1,"featured_media":24986,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve GeeTest v3 in PHP | CapSkip","rank_math_description":"GeeTest returns three values in PHP rather than one token, and the challenge dies in about a minute. Here is the flow, and why queueing breaks it.","rank_math_focus_keyword":"solve geetest in php","footnotes":""},"categories":[70],"tags":[],"class_list":["post-24987","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\/24987","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=24987"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24987\/revisions"}],"predecessor-version":[{"id":25000,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24987\/revisions\/25000"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/24986"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=24987"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=24987"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=24987"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}