如何用 PHP 和 Composer 识别 Cloudflare Turnstile

solve cloudflare turnstile in php - How to Solve Cloudflare Turnstile in PHP With Composer

Cloudflare Turnstile 有两种形式,需要不同的 PHP 处理。表单中的小组件是一个双参数调用;整页插页式挑战则需要从页面中额外读取两个值,而且只有在你回传识别工具所用的 user agent 时,token 才会被接受。省略最后这一步,你会得到一个看起来完全有效、却每次都被拒绝的 token。

你面对的是哪一种?

小组件挑战页面
外观表单中的一个复选框,仍可正常使用整页插页式,一切都被拦截
需要 cData 和 chlPageData
需要回传的 user agent

我们的 Turnstile 在线演示 运行的是小组件变体,便于对比。

设置

# PHP 8.0+, with the curl and json extensions.
composer require capskip/capskip
use CapSkip\CapSkip;

$solver = new CapSkip([
    'host' => '127.0.0.1',
    'port' => 8080,
    'recaptchaTimeout' => 300,   // seconds, also covers Turnstile
]);

CapSkip 在本地识别,因此在任何调用成功之前,桌面应用必须处于运行状态。

小组件模式

$result = $solver->turnstile(
    '0x4AAAAAAA...',                 // the data-sitekey attribute
    'https://example.com/login'
);

echo $result['code'];                // cf-turnstile-response token

$result['code'] 放入 cf-turnstile-response 字段并提交表单。这就是小组件的完整流程。

挑战页面需要另外两个值

插页式挑战携带与 token 绑定的按请求状态,其中两部分必须随识别一起传递:

  • cData,作为 data
  • chlPageData,作为 pagedata

它们位于挑战页面内部,而非表单属性中,因此必须先获取页面才能识别。在标准的 Cloudflare 插页上,它们与 sitekey 一起位于页面自身的挑战选项对象中。两者都是一次性的,且与该次页面加载绑定,因此应同时获取并识别,而不要缓存它们。

$result = $solver->turnstile($sitekey, $pageUrl, [
    'data' => $cData,             // the cData value from the page
    'pagedata' => $chlPageData,   // the chlPageData value
    'action' => 'managed',        // optional, when the page declares one
]);

echo $result['code'];
echo $result['userAgent'];        // needed for the submit

此处 user agent 是必需的

Turnstile 会将 token 绑定到生成它的浏览器指纹,而 user agent 正是该指纹的一部分。CapSkip 会在 $result['userAgent']中返回它所使用的那个。若改用 cURL 的默认 user agent 提交,Cloudflare 就会拒绝一个本来完全有效的 token。

userAgent 仅在 Turnstile 时才会被填充。对于其他所有验证码类型,它都不存在,这正是许多人复用现成的 reCAPTCHA 辅助代码时会踩坑的原因。

$ch = curl_init($pageUrl);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    // Send back the exact user agent the solve was performed with.
    CURLOPT_USERAGENT => $result['userAgent'],
    CURLOPT_POSTFIELDS => http_build_query([
        'cf-turnstile-response' => $result['code'],
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

如果一个 token 被拒绝而 cData 仍是新鲜的,几乎总是这个原因。

代理

$result = $solver->turnstile($sitekey, $pageUrl, [
    'data' => $cData,
    'pagedata' => $chlPageData,
    'proxy' => ['type' => 'HTTPS', 'uri' => 'user:[email protected]:3128'],
]);

当挑战对地理位置敏感时,请通过你将要提交的同一出口进行识别。代理适用于 Turnstile、reCAPTCHA 和极验,但不适用于图片验证码,因为后者从不接触目标站点。

错误

use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\TimeoutException;

try {
    $result = $solver->turnstile($sitekey, $pageUrl, $options);
} catch (NetworkException $e) {
    // CapSkip is not running on the configured port
} catch (ApiException $e) {
    // often a stale cData, since it is bound to one page load
} catch (TimeoutException $e) {
    // exceeded recaptchaTimeout
}

所有内容都位于 CapSkip\Exceptions之下,这与从包根目录导出异常的 Python 和 Node SDK 不同。

常见问题

小组件需要 cData 吗?

不需要,传入空值会导致识别失败,而非有所帮助。只有整页插页式挑战才会用到它们。

我的 token 有效却被拒绝。

几乎总是 user agent 的问题。将 CURLOPT_USERAGENT 设置为 $result['userAgent'] ,而不要使用 cURL 的默认值。下一个最可能的原因是 cData 过期,它只在一次页面加载内有效。

这应该在 Web 请求内部运行吗?

最好不要。PHP 是同步的,一次 Turnstile 识别需要几秒,因此在页面渲染期间执行会阻塞一个工作进程。请把它移到队列任务或 CLI 工作进程中。

小结

小组件需要 sitekey 和页面 URL。挑战页面需要 datapagedata (从页面中新鲜读取),并且 token 必须随 $result['userAgent']一起提交。请从 CapSkip\Exceptions导入异常,并让识别远离你的请求线程。

其他语言见 Cloudflare Turnstile 识别 页面,参数细节见 API 文档,更广泛的 PHP 用法见 PHP 验证码识别 页面。CapSkip 是一款 无限量验证码识别工具 ,在你自己的机器上运行。