Explainers

reCAPTCHA v2 回调机制:回调如何工作以及如何触发它们

把已识别的 token 写进 g-recaptcha-response,按钮还是灰的——解禁按钮的不是那个 textarea,而是站点注册的回调函数。所以永远是两步:先提交 token,再触发回调。本文只涉及你自有或已授权的 QA、staging 环境。

先对一下现象

现象 原因 处理方式
token 写了,按钮仍是灰的 回调没被调用 定位回调名并调用
ReferenceError: function not defined 回调在闭包里 ___grecaptcha_cfg 兜底
token 写了,AJAX 没发出 回调发的是 AJAX 读一遍回调函数体
服务端校验失败 token 已过期 识别挪到临近提交处

回调是站点注册的,不是 Google 写的

<div class="g-recaptcha"
     data-sitekey="6Le-SITEKEY"
     data-callback="onCaptchaSuccess"
     data-expired-callback="onCaptchaExpired">
</div>

<script>
function onCaptchaSuccess(token) {
  document.getElementById('submit-btn').disabled = false;
  document.getElementById('captcha-token').value = token;
}
</script>

挑战通过时 Google 调用 onCaptchaSuccess(token),token 是唯一入参,函数体由站点自己写。

三种方法定位回调名

方法一:读 data-callback 属性

// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);

回调名可能随站点构建变化,别写死,每次运行现取。

方法二:翻 grecaptcha.render 的参数

// Search page source for grecaptcha.render
document.querySelectorAll('script:not([src])').forEach(s => {
  if (s.textContent.includes('grecaptcha.render')) {
    console.log(s.textContent.match(/callback\s*:\s*(\w+)/)?.[1]);
  }
});

有的站点不写属性,而是在 grecaptcha.render() 里传 callback,名字藏在内联脚本里。

方法三:在渲染前劫持 render

const origRender = grecaptcha.render;
grecaptcha.render = function(container, params) {
  console.log('Render callback:', params.callback);
  console.log('Expired callback:', params['expired-callback']);
  return origRender.apply(this, arguments);
};

bundle 被 Webpack/Vite 压缩后翻源码没意义,而 render 的入参始终原样传进来。

完整链路:提交 token 再触发回调

  1. 取 sitekey 与回调名;
  2. in.php 提交任务;
  3. 轮询 res.php 拿 token;
  4. 写进 textarea;
  5. 调用回调。

Python(Selenium)

import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Extract sitekey and callback
sitekey = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")

callback = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-callback")

# Solve with CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": driver.current_url,
    "json": "1",
}).json()
task_id = resp["request"]

token = None
for _ in range(24):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1"
    }).json()
    if result["status"] == 1:
        token = result["request"]
        break

# Inject token into textarea
driver.execute_script("""
    document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
""", token)

# Trigger the callback
if callback:
    driver.execute_script(f"window['{callback}'](arguments[0]);", token)
    print(f"Triggered callback: {callback}")
else:
    # Fallback: try ___grecaptcha_cfg
    driver.execute_script("""
        try {
            var widgetId = Object.keys(___grecaptcha_cfg.clients)[0];
            var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
            if (typeof callback === 'function') callback(arguments[0]);
        } catch(e) {}
    """, token)
    print("Triggered callback via ___grecaptcha_cfg")

___grecaptcha_cfg 是兜底:回调在闭包里、window 上取不到时才用,依赖未公开结构,务必包在 try 里。

JavaScript(Puppeteer)

const puppeteer = require('puppeteer');

// After solving and getting the token...
await page.evaluate((token, callbackName) => {
  // Set textarea value
  const textarea = document.querySelector(
    'textarea[name="g-recaptcha-response"]'
  );
  textarea.value = token;
  textarea.style.display = 'block'; // sometimes hidden

  // Trigger callback
  if (callbackName && typeof window[callbackName] === 'function') {
    window[callbackName](token);
    console.log(`Called ${callbackName}()`);
  } else {
    // Fallback: search grecaptcha config
    try {
      const clients = ___grecaptcha_cfg.clients;
      const widgetId = Object.keys(clients)[0];
      const cb = clients[widgetId]?.aa?.l?.callback;
      if (typeof cb === 'function') cb(token);
    } catch (e) {}
  }
}, token, callbackName);

textarea.style.display = 'block' 是有意为之:部分前端校验会跳过不可见字段。

没有 data-callback 时怎么办

有的站点在提交时才调 grecaptcha.getResponse(),覆盖它更直接:

driver.execute_script("""
    const token = arguments[0];
    document.querySelector('textarea[name="g-recaptcha-response"]').value = token;
    // Override getResponse to return the token
    if (typeof grecaptcha !== 'undefined') {
        grecaptcha.getResponse = function() { return token; };
    }
""", token)

# Then submit the form normally
driver.find_element(By.CSS_SELECTOR, "form").submit()

getResponse 也没被调用,说明校验全在服务端,token 随表单发出去即可。

别忽略过期回调

token 有有效期,过期后 data-expired-callback 会把按钮重新锁上,表现为“单步能跑通,多几步就挂”。

// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
  ?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);

两条建议:

  • 把识别挪到离提交最近处:其它字段填完再提交任务,拿到 token 立刻触发回调。
  • 国内站点多用极验一类方案,reCAPTCHA 多出现在出海项目上;它的脚本走 Google 域名,加载越慢 token 窗口越紧。

常见问题

隐形 reCAPTCHA 也是这套机制吗?

同一套 data-callback,但没有勾选框,由 grecaptcha.execute() 触发,不必等点击。

回调调完了,服务端还是判定无效?

先看 token 是否过期,再核对提交任务用的 sitekey、pageurl 与实际提交页是否一致,对不上服务端一定不认。

页面上有多个 widget,回调该调哪个?

每个 widget 各带自己的 data-callback,按容器逐个取即可。

反复调试,额度够用吗?

CaptchaAI 按并发线程计费,套餐内识别次数不限。单机调试用 BASIC($15/月,5 线程)够了,接进 CI 并行跑再看 STANDARD($30/月,15 线程)。

把回调链路跑通

在自有环境跑通“提交 token → 触发回调 → 表单放行”,其它 reCAPTCHA v2 页面都是同一套模式。注册:CaptchaAI 官网

相关阅读

该文章已禁用评论。