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:
- Bắt sự kiện mở modal (nhấp trigger hoặc
MutationObservercho modal tự mở). - Đợi widget CAPTCHA render bên trong modal.
- Gọi CaptchaAI để giải và lấy token.
- Chèn token ngược vào DOM trước khi modal đóng hoặc phiên hết hạn.
Modal hay xuất hiện CAPTCHA ở đâu
- 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-sitekeytrê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
Modal CAPTCHA tự mở, không cần nhấp nút — làm sao phát hiện được?
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
- Giải reCAPTCHA v2 callback bằng API
- So sánh GeeTest và Cloudflare Turnstile
- Xử lý reCAPTCHA v2 và Turnstile trên cùng một trang
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ó.
- Xử lý nhiều CAPTCHA trên một trang
- Xử lý CAPTCHA trong Shadow DOM
- Trích xuất CAPTCHA từ iframe lồng nhau