So Sánh

So sánh reCAPTCHA v2 checkbox và reCAPTCHA Invisible

Script giải reCAPTCHA v2 của bạn chạy tốt trên một form, nhưng sang form khác thì server luôn từ chối dù CaptchaAI vẫn trả token về bình thường. Đó gần như luôn là dấu hiệu của biến thể Invisible. Hai biến thể dùng chung định dạng sitekey và cùng trả token vào trường g-recaptcha-response; khác biệt nằm ở hai chỗ: khi gửi task phải thêm invisible=1, và khi có token bạn gần như luôn phải gọi hàm callback thay vì chỉ gán vào trường ẩn.

Phần dưới đi theo đúng thứ tự thao tác khi tích hợp, kết lại bằng một solver dùng chung.

Bốn bước tích hợp và chỗ hai biến thể tách nhau

  1. Đọc HTML để xác định biến thể — bước này quyết định mọi thứ phía sau.
  2. Gửi task tới in.php, thêm invisible=1 nếu là biến thể ẩn.
  3. Polling res.php tới khi nhận OK| kèm token.
  4. Chèn token: checkbox chỉ cần gán trường ẩn, Invisible còn phải gọi callback.

reCAPTCHA v2 checkbox và Invisible hiện ra thế nào trong HTML

Dấu hiệu nằm ở các thuộc tính đi kèm phần tử g-recaptcha.

Biến thể checkbox

<!-- Standard checkbox widget -->
<div class="g-recaptcha"
     data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
     data-callback="onSubmit">
</div>

<!-- Widget renders as: -->
<!-- [✓] I'm not a robot     reCAPTCHA logo -->
  • Người dùng thấy và click trực tiếp vào ô "I'm not a robot".
  • Khi Google nghi ngờ, lưới hình ảnh hiện ngay dưới widget.

Biến thể Invisible

<!-- Pattern 1: Invisible widget on a button -->
<button class="g-recaptcha"
        data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
        data-callback="onSubmit"
        data-size="invisible">
  Submit
</button>

<!-- Pattern 2: Invisible div (programmatic trigger) -->
<div class="g-recaptcha"
     data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
     data-size="invisible"
     data-callback="onSubmit">
</div>

<!-- Pattern 3: Programmatic render -->
<script>
  grecaptcha.render('submit-btn', {
    sitekey: '6Le-wvkSAAAAAPBMRTvw...',
    callback: onSubmit,
    size: 'invisible'
  });
</script>

Không có gì hiển thị trên trang. reCAPTCHA chỉ chạy khi người dùng chạm vào phần tử đã gắn, hoặc khi code gọi grecaptcha.execute().

Bảng so sánh reCAPTCHA v2 checkbox và Invisible

Các khác biệt còn lại đều là hệ quả của việc có widget hay không.

Tiêu chí v2 checkbox v2 Invisible
Widget hiển thị Có — ô "I'm not a robot" Không
Cách kích hoạt Người dùng click vào checkbox Click nút, submit form hoặc tải trang
Thử thách hình ảnh Hiện inline khi Google nghi ngờ bot Popup góc dưới bên phải
Cách nhúng Div g-recaptcha trên trang data-callback trên nút hoặc grecaptcha.execute()
Trường token g-recaptcha-response g-recaptcha-response
Hàm callback Tùy chọn Gần như luôn bắt buộc
Tham số CaptchaAI method=userrecaptcha method=userrecaptcha + invisible=1
Hạn dùng của token 120 giây 120 giây
Thời gian giải qua CaptchaAI <60 giây <30 giây

Dòng 120 giây hay bị hiểu nhầm: token hết hạn tính từ lúc Google phát hành, không phải từ lúc res.php trả về.

Phát hiện biến thể bằng code trước khi gửi task

Viết cứng một biến thể là nguyên nhân phổ biến nhất khiến bộ test gãy khi trang đổi cấu hình.

import requests
from bs4 import BeautifulSoup
import re

def detect_recaptcha_variant(url):
    resp = requests.get(url)
    soup = BeautifulSoup(resp.text, "html.parser")

    # Check for invisible indicators
    invisible_widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
    if invisible_widget:
        return {
            "variant": "invisible",
            "sitekey": invisible_widget.get("data-sitekey"),
            "callback": invisible_widget.get("data-callback")
        }

    # Check for programmatic invisible in scripts
    for script in soup.find_all("script"):
        if script.string and "invisible" in str(script.string):
            key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
            if key_match:
                return {
                    "variant": "invisible-programmatic",
                    "sitekey": key_match.group(1),
                    "callback": "check grecaptcha.render() call"
                }

    # Check for standard checkbox
    checkbox_widget = soup.find(class_="g-recaptcha")
    if checkbox_widget:
        return {
            "variant": "checkbox",
            "sitekey": checkbox_widget.get("data-sitekey"),
            "callback": checkbox_widget.get("data-callback")
        }

    return None

result = detect_recaptcha_variant("https://staging.example.com/qa-login")
print(result)
  • Hàm trả về cả sitekey lẫn tên callback, nên bước chèn token không phải đọc lại DOM.
  • Nhánh invisible-programmatic lo trường hợp widget render bằng script.
const axios = require("axios");
const cheerio = require("cheerio");

async function detectRecaptchaVariant(url) {
  const { data } = await axios.get(url);
  const $ = cheerio.load(data);

  // Check for invisible
  const invisible = $(".g-recaptcha[data-size='invisible']");
  if (invisible.length) {
    return {
      variant: "invisible",
      sitekey: invisible.attr("data-sitekey"),
      callback: invisible.attr("data-callback"),
    };
  }

  // Check scripts for programmatic invisible
  const scripts = $("script")
    .map((_, el) => $(el).html())
    .get()
    .join("\n");
  if (scripts.includes("invisible")) {
    const keyMatch = scripts.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
    if (keyMatch) {
      return {
        variant: "invisible-programmatic",
        sitekey: keyMatch[1],
        callback: "check render call",
      };
    }
  }

  // Check for standard checkbox
  const checkbox = $(".g-recaptcha");
  if (checkbox.length) {
    return {
      variant: "checkbox",
      sitekey: checkbox.attr("data-sitekey"),
      callback: checkbox.attr("data-callback"),
    };
  }

  return null;
}

Khi debug thủ công, mở DevTools và chạy một dòng là đủ:

const el = document.querySelector('[data-size="invisible"]');
console.log(el ? "Invisible reCAPTCHA" : "Checkbox reCAPTCHA");
Tín hiệu v2 checkbox v2 Invisible
Thuộc tính data-size="invisible" Không có
Checkbox nhìn thấy được trên trang Không
Lời gọi grecaptcha.execute() Không (chờ người dùng click) Có (chạy bằng code)
Vị trí popup thử thách Inline, ngay dưới checkbox Góc dưới bên phải màn hình

Gửi task tới CaptchaAI: chỉ khác đúng một tham số

Hai đoạn dưới giống nhau từng dòng, trừ một tham số.

Với biến thể checkbox

import requests
import time

resp = requests.get("https://ocr.captchaai.com/in.php", params={
    "key": "YOUR_API_KEY",
    "method": "userrecaptcha",
    "googlekey": "6Le-wvkSAAAA...",
    "pageurl": "https://example.com/form"
})
task_id = resp.text.split("|")[1]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": "YOUR_API_KEY", "action": "get", "id": task_id
    })
    if result.text.startswith("OK|"):
        token = result.text.split("|")[1]
        break

Với biến thể Invisible

import requests
import time

resp = requests.get("https://ocr.captchaai.com/in.php", params={
    "key": "YOUR_API_KEY",
    "method": "userrecaptcha",
    "googlekey": "6Le-wvkSAAAA...",
    "pageurl": "https://example.com/form",
    "invisible": 1  # Only parameter difference
})
task_id = resp.text.split("|")[1]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": "YOUR_API_KEY", "action": "get", "id": task_id
    })
    if result.text.startswith("OK|"):
        token = result.text.split("|")[1]
        break

Khác biệt duy nhất là "invisible": 1. Phần còn lại của luồng, gồm cả cách xử lý mã lỗi từ res.php, có trong hướng dẫn giải reCAPTCHA v2 bằng API.

Chèn token vào trang — chỗ dễ sai nhất

Đây là nơi hai biến thể thực sự tách nhau, và cũng tốn thời gian debug nhất.

checkbox: gán vào trường ẩn thường là đủ

# Selenium — inject into hidden field
driver.execute_script(
    f'document.getElementById("g-recaptcha-response").value = "{token}";'
)

# If the page uses a callback, also call it
callback = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
if callback:
    driver.execute_script(f'{callback}("{token}");')

Cú click của người dùng đã đăng ký sẵn callback, nên form chấp nhận token ngay.

Invisible: gần như luôn phải gọi callback

# Selenium — inject AND call the callback
driver.execute_script(
    f'document.getElementById("g-recaptcha-response").value = "{token}";'
)

# CRITICAL: Invisible reCAPTCHA almost always requires calling the callback
callback_name = driver.find_element(
    "css selector", ".g-recaptcha[data-size='invisible']"
).get_attribute("data-callback")

driver.execute_script(f'{callback_name}("{token}");')
// Puppeteer — invisible callback injection
await page.evaluate((tok) => {
  // Set the hidden field
  document.getElementById("g-recaptcha-response").value = tok;

  // Find and call the callback function
  const widget = document.querySelector("[data-size='invisible']");
  const cbName = widget?.getAttribute("data-callback");
  if (cbName && typeof window[cbName] === "function") {
    window[cbName](tok);
  }
}, token);

Điểm mấu chốt: Invisible không có cú click đó. Không gọi callback tường minh thì trang coi như chưa ai vượt qua thử thách, dù g-recaptcha-response đã có token hợp lệ.

Một solver dùng chung cho cả hai biến thể

Trong dự án thật bạn hiếm khi biết trước trang dùng biến thể nào, nên hãy gộp bước phát hiện vào bước giải.

import requests
import time
from bs4 import BeautifulSoup

class RecaptchaV2UniversalSolver:
    def __init__(self, api_key):
        self.api_key = api_key

    def detect_and_solve(self, page_url, page_html=None):
        if not page_html:
            page_html = requests.get(page_url).text

        soup = BeautifulSoup(page_html, "html.parser")

        # Detect variant
        invisible = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
        widget = invisible or soup.find(class_="g-recaptcha")

        if not widget:
            raise Exception("No reCAPTCHA widget found")

        sitekey = widget.get("data-sitekey")
        is_invisible = invisible is not None
        callback = widget.get("data-callback")

        params = {
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url
        }
        if is_invisible:
            params["invisible"] = 1

        resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
        if not resp.text.startswith("OK|"):
            raise Exception(f"Submit failed: {resp.text}")

        task_id = resp.text.split("|")[1]
        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get", "id": task_id
            })
            if result.text.startswith("OK|"):
                return {
                    "token": result.text.split("|")[1],
                    "variant": "invisible" if is_invisible else "checkbox",
                    "callback": callback,
                    "sitekey": sitekey
                }
            if result.text != "CAPCHA_NOT_READY":
                raise Exception(f"Solve error: {result.text}")

        raise Exception("Timed out")

# Usage
solver = RecaptchaV2UniversalSolver("YOUR_API_KEY")
result = solver.detect_and_solve("https://staging.example.com/qa-login")
print(f"Variant: {result['variant']}, Callback: {result['callback']}")

Lợi ích: chỉ còn một chỗ cần sửa khi trang đổi cấu hình, thay vì hai nhánh code dễ lệch nhau.

Tình huống thường gặp ở đội QA tại Việt Nam

Một công ty outsourcing ở TP.HCM chạy regression hằng đêm cho form đăng ký của khách hàng. Bản desktop dùng checkbox, bản mobile cấu hình Invisible, vẫn cùng một sitekey. Bộ test viết cứng theo nhánh checkbox chạy xanh trên desktop và đỏ toàn bộ trên mobile, còn log chỉ ghi "form submit failed".

  1. Chạy hàm phát hiện ở đầu mỗi test case, đừng lưu biến thể vào file cấu hình.
  2. Ghi biến thể phát hiện được vào log để tách lỗi tích hợp khỏi lỗi ứng dụng.
  3. Chỉ chạy trên staging của hệ thống đội bạn được phép kiểm thử, và giữ log tối thiểu — Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân là lý do để không đổ dữ liệu form vào log.

Chi phí có khác nhau giữa hai biến thể không

Không. CaptchaAI tính tiền theo thread (luồng giải đồng thời) chứ không theo từng lượt giải, nên hoá đơn phụ thuộc vào số task chạy song song. Gói BASIC ($15/tháng, 5 thread) đủ cho một bộ regression nhỏ chạy tuần tự; muốn chạy nhiều suite cùng lúc trong cửa sổ ban đêm thì STANDARD ($30/tháng, 15 thread) là bậc tiếp theo.

Lỗi thường gặp và cách xử lý

Triệu chứng Với checkbox Với Invisible
Server từ chối token Gán token vào g-recaptcha-response Gán token, rồi gọi hàm trong data-callback
Không tìm thấy widget Tìm class .g-recaptcha Tìm data-size="invisible" hoặc lời gọi render
Form submit xong vẫn lỗi Xem trang có chờ callback không Tìm tên hàm callback rồi gọi
Trang tự tải lại sau khi chèn Validation JavaScript không qua Gọi callback trước khi form tự submit

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

Làm sao biết trang dùng biến thể nào khi không thấy widget nào cả?

Tìm data-size="invisible" trong HTML, hoặc lời gọi grecaptcha.render()size: 'invisible' trong thẻ script. Không thấy widget chưa chắc đã là Invisible: widget checkbox cũng có thể bị CSS che.

Token của hai biến thể có dùng thay cho nhau được không?

Không. Token gắn với sitekey, domain và phiên phát hành ra nó. Cả hai đều có hạn 120 giây, nên hãy submit form ngay sau khi nhận token.

Vì sao form vẫn lỗi dù token đã được chèn thành công?

Gần như luôn là do chưa gọi callback. Gán giá trị vào g-recaptcha-response chỉ điền dữ liệu vào DOM chứ không kích hoạt logic xác thực của trang.

Biến thể nào giải nhanh hơn?

Invisible thường nhanh hơn: mức trần <30 giây so với <60 giây của checkbox, do ít khi phát sinh thử thách hình ảnh. Với polling 5 giây một lần, chênh lệch này ít khi đáng kể.

Đọc thêm

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