Phân Tích Kỹ Thuật

Callback của reCAPTCHA v2: cách hoạt động và cách kích hoạt sau khi chèn token

Token đúng, textarea g-recaptcha-response đã có giá trị, nhưng nút gửi vẫn xám — đây là lỗi phổ biến nhất khi tự động hóa reCAPTCHA v2. Nguyên nhân gần như luôn là: bạn mới điền token mà chưa gọi callback, hàm JavaScript do trang web đăng ký. Khi người dùng thật giải xong, script của Google gọi hàm đó, và chính nó mới mở khóa nút gửi hoặc bắn request AJAX. Chèn token là điều kiện cần, gọi callback là điều kiện đủ.

Nhận diện nhanh: triệu chứng nào ứng với nguyên nhân nào

Vấn đề Nguyên nhân Cách xử lý
Form vẫn bị khóa sau khi chèn token Chưa gọi callback Đọc data-callback và gọi hàm đó
ReferenceError: function not defined Callback nằm trong closure, không gắn vào window Dùng nhánh dự phòng ___grecaptcha_cfg
Token đã chèn nhưng không có request AJAX nào Callback bắn AJAX chứ không submit form Đọc nội dung hàm callback để biết nó làm gì
Server trả lỗi dù token được chấp nhận ở client Token hết hạn trước khi submit Giải sát thời điểm submit
Trang có nhiều widget, chỉ một cái hoạt động Kịch bản lấy nhầm widget đầu tiên Duyệt từng .g-recaptcha và xử lý theo widgetId

Callback được đăng ký như thế nào

Trang web khai báo callback ngay trên widget reCAPTCHA:

<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>

Khi thử thách được giải, JavaScript của Google gọi onCaptchaSuccess(token) với một đối số duy nhất: chuỗi token. Toàn bộ logic mở khóa form nằm bên trong hàm này chứ không ở giá trị textarea. Bạn sẽ gặp ba dạng đăng ký:

  • data-callback trên thẻ widget — phổ biến nhất, tên hàm nằm sẵn trong HTML.
  • Tùy chọn callback truyền vào grecaptcha.render() — hàm có thể ẩn danh, không gắn vào window.
  • Không đăng ký callback nào — form tự đọc grecaptcha.getResponse() ở bước submit.

Ba cách tìm đúng tên hàm callback

Đừng hardcode tên hàm: nhiều site sinh nó động hoặc giấu trong closure sau khi bundler đóng gói.

Cách 1: đọc thuộc tính data-callback

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

Cách 2: soi phần khởi tạo grecaptcha.render

Một số site truyền callback qua tùy chọn của 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]);
  }
});

Cách 3: chặn ngay lúc widget đăng ký

Dùng khi hai cách trên trả về rỗng. Chạy trong DevTools trước khi trang tải (Sources → Snippets):

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);
};

Kích hoạt callback sau khi chèn token

Python (Selenium)

Luồng đầy đủ: lấy sitekey và tên callback từ DOM, gửi task tới in.php, polling res.php, ghi token vào textarea, rồi gọi hàm.

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")

Nhánh else mới đáng chú ý: không có data-callback thì script đọc ___grecaptcha_cfg — cấu trúc nội bộ reCAPTCHA giữ cho từng widget — rồi gọi hàm lấy được.

JavaScript (Puppeteer)

Cùng logic, gói trong một page.evaluate():

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);

Khi trang không khai báo data-callback

Không phải site nào cũng dùng callback. Nhiều form chỉ gọi grecaptcha.getResponse() lúc submit; cách gọn nhất là ghi đè hàm đó:

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()

Expired callback và bẫy token hết hạn

Nhiều site khai báo thêm data-expired-callback để khóa lại nút gửi khi token hết hiệu lực. Giải sớm rồi chờ vài phút mới submit thì hàm này có thể chạy và khóa form ngay sau khi bạn vừa mở được nó — trong log nhìn hệt lỗi "callback không chạy". Xử lý: đẩy bước giải xuống sát lúc submit.

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

Ví dụ thực tế: suite QA của một team outsourcing tại TP.HCM

Một team QA làm dự án web cho khách nước ngoài chạy khoảng 300 lượt regression test đăng nhập mỗi đêm trên staging có bật reCAPTCHA v2. Bản Selenium đầu tiên chèn token rồi gọi submit(), và khoảng một phần ba số lần chạy thất bại — không vì token sai, mà vì form dùng AJAX kích hoạt từ callback nên submit() không gửi gì cả. Đọc data-callback rồi gọi window[callback](token) là hết cả lớp lỗi này.

Về chi phí, 300 lượt mỗi đêm là khối lượng nhỏ với mô hình giá theo thread của CaptchaAI: gói BASIC ($15/tháng, 5 thread) đã đủ, vì thread là số lượt giải chạy song song (luồng giải đồng thời) chứ không phải hạn mức số lần giải. Chạy nhiều môi trường cùng lúc mới cần STANDARD ($30/tháng, 15 thread).

Checklist trước khi đưa kịch bản lên CI

  1. Đọc data-callback từ DOM ở runtime, không hardcode tên hàm.
  2. Ghi token vào g-recaptcha-response trước, gọi callback sau.
  3. Chuẩn bị nhánh dự phòng ___grecaptcha_cfg cho site giấu callback trong closure.
  4. Kiểm tra data-expired-callback và rút ngắn khoảng cách giải–submit.
  5. Nếu trang có nhiều widget, xử lý theo widgetId thay vì luôn lấy phần tử đầu tiên.

Câu hỏi thường gặp

Vì sao nút gửi vẫn xám dù textarea đã có token?

Vì nút được mở khóa bên trong hàm callback, không phải bởi giá trị textarea. Hai bước cần làm sau khi chèn token:

  • Gọi window[<tên_callback>](token) nếu tên hàm đọc được từ data-callback.
  • Nếu không, dùng nhánh ___grecaptcha_cfg để lấy hàm mà widget đã đăng ký.

Làm sao biết trang dùng callback hay dùng getResponse?

Kiểm tra data-callback trên .g-recaptcha trước. Nếu rỗng, chặn grecaptcha.render như ở Cách 3; vẫn rỗng thì gần như chắc chắn form đọc grecaptcha.getResponse() lúc submit — ghi đè hàm đó.

Một trang có nhiều reCAPTCHA thì callback xử lý ra sao?

Mỗi widget có widgetId và callback riêng, đừng giả định chỉ có index 0. Trình tự đúng:

  1. Duyệt tất cả phần tử .g-recaptcha trên trang.
  2. Đọc data-callback của từng widget.
  3. Chèn token vào đúng textarea rồi gọi hàm tương ứng cho widget đó.

Token do CaptchaAI trả về có tự kích hoạt callback không?

Không. API chỉ trả chuỗi token qua res.php; đặt token vào g-recaptcha-response và gọi callback là phần bạn viết trong trình duyệt tự động.

reCAPTCHA v2 và v3 khác nhau thế nào ở khâu này?

Khía cạnh reCAPTCHA v2 reCAPTCHA v3
Widget hiển thị Không
Thuộc tính data-callback Không
Cách nhận token Callback được gọi grecaptcha.execute() trả Promise
Việc bạn phải làm sau khi có token Chèn rồi gọi callback Gửi kèm request, không cần gọi hàm nào

Bước tiếp theo và bài đọc thêm

Lấy API key tại captchaai.com, chạy đoạn Selenium ở trên và xem nút gửi có mở khóa ngay lần đầu không. Ba bài dưới đi sâu vào các tình huống liền kề:

Os comentários estão desativados para este artigo.