把已识别的 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 再触发回调
- 取 sitekey 与回调名;
- 向
in.php提交任务; - 轮询
res.php拿 token; - 写进 textarea;
- 调用回调。
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 官网。