Hướng Dẫn Thực Hành

CAPTCHA trong modal: cách phát hiện và chèn token tự động

CAPTCHA trong modal chỉ xuất hiện sau khi hộp thoại mở — sitekey không nằm trong HTML ban đầu, nên script không thể trích xuất trước khi modal bật lên. Khi người dùng nhấp "Gửi" hoặc "Đăng nhập", modal hiện ra kèm thử thách CAPTCHA, và bạn chỉ có vài giây trước khi modal tự đóng hoặc phiên hết hạn.

Ví dụ thực tế: một đội QA tại công ty outsourcing ở TP.HCM kiểm thử luồng đăng nhập nội bộ trên staging — form đăng nhập bật modal xác thực kèm reCAPTCHA v2 sau ba lần nhập sai mật khẩu. Vì sitekey chỉ xuất hiện khi modal mở, script phải đợi modal render xong rồi mới trích xuất, thay vì lấy sitekey một lần khi tải trang.

Quy trình xử lý gồm 4 bước:

  1. Bắt sự kiện mở modal (nhấp trigger hoặc MutationObserver cho modal tự mở).
  2. Đợi widget CAPTCHA render bên trong modal.
  3. Gọi CaptchaAI để giải và lấy token.
  4. Chèn token ngược vào DOM trước khi modal đóng hoặc phiên hết hạn.
  • Modal đăng nhập — nhấp nút "Đăng nhập"; CAPTCHA tải trong lớp phủ (overlay div).
  • Lớp chặn chống bot — tự bật sau hành vi đáng ngờ; modal toàn màn hình chặn cả trang.
  • Xác nhận thanh toán — gửi form thanh toán; modal hiện ra để xác minh.
  • Hộp thoại giới hạn tần suất — phát hiện quá nhiều request; modal có cổng CAPTCHA.
  • Đồng ý cookie kèm CAPTCHA — lần truy cập đầu tiên; CAPTCHA nhúng trong hộp thoại đồng ý.

Cách nhận biết modal đã sẵn sàng để giải CAPTCHA

  • Modal khớp selector phổ biến: .modal.show, dialog[open], [role="dialog"]:not([aria-hidden="true"]), .popup.active.
  • Phần tử phải thật sự hiển thị — kiểm tra el.offsetParent !== null, không chỉ dựa vào việc nó tồn tại trong DOM.
  • Đợi thêm 1–2 giây sau khi modal hiển thị — widget CAPTCHA thường render trễ hơn khung modal.
  • Tìm data-sitekey trên .g-recaptcha (reCAPTCHA) hoặc .cf-turnstile (Turnstile) bên trong modal, không phải toàn trang.

Python: phát hiện và giải CAPTCHA trong modal bằng Playwright

import requests
import time
from playwright.sync_api import sync_playwright

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"

def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
    """Submit and poll a CAPTCHA."""
    params = {
        "key": API_KEY,
        "method": method,
        "json": 1,
    }
    if method == "userrecaptcha":
        params["googlekey"] = sitekey
        params["pageurl"] = pageurl
    elif method == "turnstile":
        params["sitekey"] = sitekey
        params["pageurl"] = pageurl

    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(60):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")

def detect_modal_captcha(page):
    """
    Detect CAPTCHA inside a visible modal/dialog.
    Returns (sitekey, method) or (None, None).
    """
    return page.evaluate("""
        () => {
            // Find visible modals
            const modalSelectors = [
                '.modal.show',
                '.modal[style*="display: block"]',
                'dialog[open]',
                '[role="dialog"]:not([aria-hidden="true"])',
                '.overlay.visible',
                '.popup.active',
                '[class*="modal"][class*="open"]',
            ];

            let modal = null;
            for (const sel of modalSelectors) {
                const el = document.querySelector(sel);
                if (el && el.offsetParent !== null) {
                    modal = el;
                    break;
                }
            }

            // If no modal found, search entire document
            const searchRoot = modal || document;

            // Check for reCAPTCHA
            const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
            if (recaptcha) {
                return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
            }

            // Check for Turnstile
            const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
            if (turnstile) {
                return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
            }

            // Check for hCaptcha
            const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
            if (hcaptcha) {
                return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
            }

            return null;
        }
    """)

def inject_token_in_modal(page, token, method="userrecaptcha"):
    """Inject token into the CAPTCHA inside the modal."""
    if method == "userrecaptcha":
        page.evaluate("""
            (token) => {
                // Find response textarea (may be inside modal)
                const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
                textareas.forEach(ta => {
                    ta.value = token;
                    ta.style.display = 'block';
                });

                // Trigger callback
                if (typeof ___grecaptcha_cfg !== 'undefined') {
                    Object.values(___grecaptcha_cfg.clients).forEach(client => {
                        Object.values(client).forEach(val => {
                            if (val && typeof val === 'object') {
                                Object.values(val).forEach(v => {
                                    if (v && typeof v.callback === 'function') v.callback(token);
                                });
                            }
                        });
                    });
                }
            }
        """, token)
    elif method == "turnstile":
        page.evaluate("""
            (token) => {
                const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
                inputs.forEach(inp => { inp.value = token; });

                if (typeof window.turnstileCallback === 'function') {
                    window.turnstileCallback(token);
                }
            }
        """, token)

def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
    """
    Full workflow: trigger modal, detect CAPTCHA, solve, inject.
    """
    # Step 1: Trigger the modal if needed
    if trigger_selector:
        print(f"Clicking trigger: {trigger_selector}")
        page.click(trigger_selector)

    # Step 2: Wait for modal to become visible
    print("Waiting for modal...")
    modal_selectors = [
        ".modal.show",
        "dialog[open]",
        '[role="dialog"]:not([aria-hidden="true"])',
        ".popup.active",
    ]

    modal_visible = False
    for selector in modal_selectors:
        try:
            page.wait_for_selector(selector, timeout=timeout)
            modal_visible = True
            print(f"  Modal detected: {selector}")
            break
        except Exception:
            continue

    if not modal_visible:
        print("  No modal detected")
        return None

    # Step 3: Wait for CAPTCHA to render inside modal
    time.sleep(2)  # Brief pause for dynamic CAPTCHA loading

    captcha_info = detect_modal_captcha(page)
    if not captcha_info:
        print("  No CAPTCHA found in modal")
        return None

    sitekey = captcha_info["sitekey"]
    method = captcha_info["method"]
    print(f"  Found {method} CAPTCHA: {sitekey[:20]}...")

    # Step 4: Solve via CaptchaAI
    token = solve_captcha(sitekey, page.url, method)
    print(f"  Solved: {token[:30]}...")

    # Step 5: Inject token
    inject_token_in_modal(page, token, method)
    print("  Token injected")

    return token

def main():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        page.goto("https://example.com")
        page.wait_for_load_state("networkidle")

        # Handle CAPTCHA that appears in login modal
        token = handle_modal_captcha(
            page,
            trigger_selector="button#login-btn",
            timeout=10000,
        )

        if token:
            # Fill form fields inside modal
            page.fill('dialog input[name="email"]', "[email protected]")
            page.fill('dialog input[name="password"]', "password123")

            # Submit modal form
            page.click('dialog button[type="submit"]')
            page.wait_for_load_state("networkidle")

        browser.close()

main()

JavaScript: xử lý CAPTCHA trong modal bằng Puppeteer

const puppeteer = require("puppeteer");

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
  const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
  if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
  else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", pageurl); }

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function waitForModal(page, timeout = 10000) {
  const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
  for (const sel of selectors) {
    try {
      await page.waitForSelector(sel, { visible: true, timeout });
      return sel;
    } catch {}
  }
  return null;
}

async function detectModalCaptcha(page) {
  return page.evaluate(() => {
    const checks = [
      { sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
      { sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
    ];
    for (const { sel, method } of checks) {
      const el = document.querySelector(sel);
      if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
    }
    return null;
  });
}

async function handleModalCaptcha(page, triggerSelector) {
  // Trigger modal
  if (triggerSelector) await page.click(triggerSelector);

  // Wait for modal
  const modalSel = await waitForModal(page);
  if (!modalSel) { console.log("No modal found"); return null; }
  console.log(`Modal visible: ${modalSel}`);

  // Wait for CAPTCHA render
  await new Promise((r) => setTimeout(r, 2000));

  const info = await detectModalCaptcha(page);
  if (!info) { console.log("No CAPTCHA in modal"); return null; }

  console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
  const token = await solveCaptcha(info.sitekey, page.url(), info.method);
  console.log(`Solved: ${token.substring(0, 30)}...`);

  // Inject token
  await page.evaluate((t, method) => {
    if (method === "userrecaptcha") {
      document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
    } else if (method === "turnstile") {
      document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
    }
  }, token, info.method);

  return token;
}

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto("https://example.com", { waitUntil: "networkidle2" });

  const token = await handleModalCaptcha(page, "button#login-btn");
  if (token) {
    await page.type('dialog input[name="email"]', "[email protected]");
    await page.click('dialog button[type="submit"]');
    await page.waitForNavigation();
  }

  await browser.close();
})();

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

Dùng MutationObserver để theo dõi phần tử mới thêm vào DOM, thiết lập trước khi điều hướng tới trang. Khi modal xuất hiện và hiển thị, observer kích hoạt và bạn bắt đầu luồng phát hiện CAPTCHA.

CAPTCHA nằm trong iframe lồng bên trong modal thì xử lý sao?

Kết hợp phát hiện modal với xử lý iframe: sau khi phát hiện modal, chuyển ngữ cảnh sang iframe bên trong nó rồi mới trích sitekey. Modal và iframe chứa CAPTCHA thường tồn tại độc lập trong cây DOM.

CaptchaAI có giải được hCaptcha xuất hiện trong modal không?

Chưa. CaptchaAI hỗ trợ reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 và CAPTCHA dạng ảnh/lưới trong modal — hCaptcha và FunCaptcha (Arkose Labs) chưa được hỗ trợ. Nếu script phát hiện .h-captcha, hãy ghi log và bỏ qua thay vì gửi sang API.

Cần bao nhiêu thread để xử lý nhiều modal CAPTCHA cùng lúc?

Tùy số modal chạy song song trong test suite. Gói BASIC ($15/tháng, 5 thread) đủ cho vài luồng kiểm thử tuần tự; CI chạy nhiều modal song song nên cân nhắc ADVANCE ($90/tháng, 50 thread) để tránh nghẽn hàng đợi giải.

Turnstile trong modal có đợi lâu hơn reCAPTCHA v2 không?

Không đáng kể — cả hai render trong khoảng 1–3 giây sau khi modal mở. Khác biệt nằm ở bước trích xuất: Turnstile dùng data-sitekey trên .cf-turnstile, reCAPTCHA dùng .g-recaptcha; logic phát hiện ở phần Python/JavaScript phía trên xử lý cả hai bằng chung một hàm.

Thời điểm dễ mất token khi làm việc với modal

Yếu tố Ảnh hưởng Cách xử lý
Modal tự đóng theo timeout Modal có thể đóng trước khi giải xong CAPTCHA Bắt đầu giải ngay khi vừa phát hiện
Phiên hết hạn trong lúc giải Phiên server hết hạn khi modal đang chờ Giữ phiên sống bằng heartbeat nền
CAPTCHA render trễ trong modal Widget mất 1–3 giây để tải sau khi modal mở Đợi 2 giây sau khi modal hiển thị rồi mới trích sitekey
Token hết hạn khi đang điền form Token hết hạn trong lúc điền các trường còn lại của modal Giải CAPTCHA cuối cùng, sau khi đã điền xong các trường khác

Lỗi thường gặp khi xử lý CAPTCHA trong modal

Vấn đề Nguyên nhân Cách xử lý
Phát hiện được modal nhưng không thấy CAPTCHA CAPTCHA tải bất đồng bộ sau khi modal mở Tăng thời gian chờ; dùng MutationObserver để bắt widget mới chèn vào DOM
Đã chèn token nhưng modal không đóng Callback của CAPTCHA chưa được gọi Tìm và gọi callback CAPTCHA một cách tường minh
Modal đóng ngay giữa lúc đang giải Timeout tự động đóng modal Vô hiệu hóa timeout của modal bằng JS: clearTimeout() trên bộ đếm giờ modal
Sitekey đổi mỗi lần mở modal Modal khởi tạo instance CAPTCHA mới mỗi lần Luôn trích sitekey mới từ DOM của modal, không cache lại
Nhấp trigger nhưng modal không mở Phần tử chưa tương tác được hoặc bị lớp phủ che Dùng page.dispatchEvent hoặc đợi phần tử có thể click được

Bài viết liên quan

Bước tiếp theo

Lấy API key CaptchaAI và áp dụng luồng phát hiện modal ở trên vào script của bạn trong vài phút, không cần đổi lại luồng UI hiện có.

  1. Xử lý nhiều CAPTCHA trên một trang
  2. Xử lý CAPTCHA trong Shadow DOM
  3. Trích xuất CAPTCHA từ iframe lồng nhau
Os comentários estão desativados para este artigo.