document.querySelector tiêu chuẩn không thể tiếp cận được bên trong rễ bóng. Khi tiện ích CAPTCHA được hiển thị bên trong Shadow DOM của thành phần web, tập lệnh trích xuất khóa trang web thông thường của bạn sẽ trả về null. CAPTCHA có ở đó - bộ chọn của bạn không thể nhìn thấy nó.
Tại sao CAPTCHA lại xuất hiện trong Shadow DOM
| Kịch bản | Lý do |
|---|---|
| Thành phần đăng nhập tùy chỉnh | Được đóng gói dưới dạng thành phần web có thể tái sử dụng |
| Tiện ích biểu mẫu của bên thứ ba | Nhà cung cấp widget bao bọc toàn bộ biểu mẫu trong root root |
| Kiến trúc micro-frontend | Mỗi ứng dụng vi mô sử dụng DOM bóng riêng biệt |
| Thiết kế các thành phần hệ thống | CAPTCHA được nhúng trong phần tử thư viện thành phần |
Phát hiện Shadow DOM CAPTCHA
Trước khi giải, hãy xác nhận CAPTCHA nằm trong root root:
// In browser DevTools console
// Regular query returns null even though CAPTCHA is visible
document.querySelector('.cf-turnstile'); // null
// Check for shadow hosts
document.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) {
const captcha = el.shadowRoot.querySelector('.cf-turnstile, .g-recaptcha');
if (captcha) {
console.log('Found CAPTCHA in shadow root of:', el.tagName, el.id || el.className);
console.log('Sitekey:', captcha.dataset.sitekey);
}
}
});
Python: Nhà viết kịch Shadow DOM Piercing
Bộ chọn xuyên thấu >> của Playwright và API locator xử lý DOM bóng nguyên bản:
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_turnstile(sitekey, pageurl):
"""Submit and poll a Turnstile CAPTCHA."""
resp = requests.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
"json": 1,
}, 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 extract_from_shadow_dom(page):
"""Extract CAPTCHA sitekey from shadow DOM elements."""
# Method 1: Playwright's piercing selector (>>)
# This automatically crosses shadow boundaries
turnstile = page.locator("css=.cf-turnstile >> visible=true").first
if turnstile.count() > 0:
sitekey = turnstile.get_attribute("data-sitekey")
if sitekey:
return sitekey
# Method 2: JavaScript evaluation to pierce all shadow roots
sitekey = page.evaluate("""
() => {
function findInShadowRoots(root) {
// Check direct children
const turnstile = root.querySelector('.cf-turnstile');
if (turnstile && turnstile.dataset.sitekey) {
return turnstile.dataset.sitekey;
}
const recaptcha = root.querySelector('.g-recaptcha');
if (recaptcha && recaptcha.dataset.sitekey) {
return recaptcha.dataset.sitekey;
}
// Recurse into nested shadow roots
for (const el of root.querySelectorAll('*')) {
if (el.shadowRoot) {
const found = findInShadowRoots(el.shadowRoot);
if (found) return found;
}
}
return null;
}
return findInShadowRoots(document);
}
""")
return sitekey
def inject_token_shadow_dom(page, token, captcha_type="turnstile"):
"""Inject solved token into shadow DOM CAPTCHA element."""
if captcha_type == "turnstile":
page.evaluate(f"""
(token) => {{
function findAndInject(root) {{
// Find the response input inside Turnstile
const input = root.querySelector('[name="cf-turnstile-response"]');
if (input) {{
input.value = token;
return true;
}}
// Recurse into shadow roots
for (const el of root.querySelectorAll('*')) {{
if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
return true;
}}
}}
return false;
}}
findAndInject(document);
// Also try callback if defined
if (typeof window.turnstileCallback === 'function') {{
window.turnstileCallback(token);
}}
}}
""", token)
elif captcha_type == "recaptcha":
page.evaluate(f"""
(token) => {{
function findAndInject(root) {{
const textarea = root.querySelector('#g-recaptcha-response');
if (textarea) {{
textarea.value = token;
textarea.style.display = 'block';
return true;
}}
for (const el of root.querySelectorAll('*')) {{
if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
return true;
}}
}}
return false;
}}
findAndInject(document);
if (typeof ___grecaptcha_cfg !== 'undefined') {{
Object.entries(___grecaptcha_cfg.clients).forEach(([_, client]) => {{
Object.entries(client).forEach(([_, val]) => {{
if (val && typeof val === 'object') {{
Object.entries(val).forEach(([_, v]) => {{
if (v && v.callback) v.callback(token);
}});
}}
}});
}});
}}
}}
""", token)
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://staging.example.com/qa-login")
page.wait_for_load_state("networkidle")
# Extract sitekey from shadow DOM
sitekey = extract_from_shadow_dom(page)
if not sitekey:
print("No CAPTCHA found in shadow DOM or regular DOM")
browser.close()
return
print(f"Found sitekey: {sitekey}")
# Solve via CaptchaAI
token = solve_turnstile(sitekey, page.url)
print(f"Solved: {token[:40]}...")
# Inject token back into shadow DOM
inject_token_shadow_dom(page, token, "turnstile")
print("Token injected into shadow DOM")
# Submit the form
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")
browser.close()
main()
JavaScript: Truyền tải bóng tối của 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 solveTurnstile(sitekey, pageurl) {
const params = new URLSearchParams({
key: API_KEY, method: "turnstile", sitekey, pageurl, json: "1",
});
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body: params })).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 extractSitekeyFromShadowDOM(page) {
return page.evaluate(() => {
function searchShadowRoots(root) {
const selectors = [".cf-turnstile", ".g-recaptcha", ".h-captcha"];
for (const sel of selectors) {
const el = root.querySelector(sel);
if (el && el.dataset.sitekey) return el.dataset.sitekey;
}
for (const el of root.querySelectorAll("*")) {
if (el.shadowRoot) {
const found = searchShadowRoots(el.shadowRoot);
if (found) return found;
}
}
return null;
}
return searchShadowRoots(document);
});
}
async function injectTokenShadowDOM(page, token) {
await page.evaluate((t) => {
function inject(root) {
const input = root.querySelector('[name="cf-turnstile-response"]');
if (input) { input.value = t; return true; }
const textarea = root.querySelector("#g-recaptcha-response");
if (textarea) { textarea.value = t; return true; }
for (const el of root.querySelectorAll("*")) {
if (el.shadowRoot && inject(el.shadowRoot)) return true;
}
return false;
}
inject(document);
}, token);
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://staging.example.com/qa-login", { waitUntil: "networkidle2" });
const sitekey = await extractSitekeyFromShadowDOM(page);
if (!sitekey) {
console.log("No CAPTCHA found in shadow DOM");
await browser.close();
return;
}
console.log(`Sitekey: ${sitekey}`);
const token = await solveTurnstile(sitekey, page.url());
console.log(`Solved: ${token.substring(0, 40)}...`);
await injectTokenShadowDOM(page, token);
await page.click('button[type="submit"]');
await page.waitForNavigation();
await browser.close();
})();
Cân nhắc độ sâu của Shadow DOM
| Độ sâu | Ví dụ | Cách tiếp cận |
|---|---|---|
| 1 cấp độ | <custom-form> #shadow-root > .cf-turnstile |
Truy vấn gốc bóng trực tiếp |
| 2+ cấp độ | <app-shell> #shadow > <login-form> #shadow > .cf-turnstile |
Truyền tải đệ quy |
| Mở gốc bóng | Có thể truy cập el.shadowRoot |
Cách tiếp cận tiêu chuẩn - sử dụng đệ quy |
| Gốc bóng đóng | el.shadowRoot trả về giá trị rỗng |
Không thể xuyên thủng; sử dụng page.evaluate với tính năng ghi đè hoặc chặn hiển thị attachShadow({mode:'open'}) |
Thứ tự chẩn đoán dự phòng
- Trước tiên, hãy kiểm tra các gốc bóng mở, sau đó là ranh giới iframe và chỉ sau đó là các cuộc gọi lại cấp ứng dụng hoặc các móc được chèn.
- Ghi lại lớp nào đã bộc lộ thách thức để dễ dàng phát hiện những thay đổi mục tiêu trong tương lai.
- Giữ một phương án dự phòng cuối cùng cho các gốc đã đóng thay vì giả sử mọi trang đều hiển thị quyền truy cập DOM một cách thống nhất.
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
shadowRoot trả về null |
DOM bóng đóng | Ghi đè attachShadow trước khi tải trang để buộc mode: 'open' |
| Đã tìm thấy khóa trang web nhưng mã thông báo bị từ chối | URL trang sai được gửi đến người giải quyết | Sử dụng page.url() tại thời điểm trích xuất, không phải URL được mã hóa cứng |
| Mã thông báo đã được chèn nhưng biểu mẫu không gửi được | Lệnh gọi lại CAPTCHA không được kích hoạt | Tìm và gọi hàm gọi lại sau khi đặt giá trị mã thông báo |
| Tìm kiếm đệ quy chậm | Cây DOM sâu | Giới hạn độ sâu đệ quy; nhắm mục tiêu tên thẻ máy chủ bóng đã biết |
| Không tìm thấy CAPTCHA trong lần tải đầu tiên | Tải widget không đồng bộ | Đợi phần tử máy chủ bóng trước: page.waitForSelector('custom-form') |
Câu hỏi thường gặp
Làm cách nào để biết CAPTCHA có sử dụng Shadow DOM hay không?
Trong Chrome DevTools, hãy kiểm tra phần tử CAPTCHA. Nếu bạn thấy #shadow-root (open) hoặc #shadow-root (closed) phía trên đánh dấu CAPTCHA thì nó nằm trong gốc bóng. querySelector thông thường từ document sẽ không tìm thấy nó.
Tôi có thể tránh hoàn toàn việc truyền tải bóng DOM không?
Nếu bạn đang sử dụng trực tiếp API CaptchaAI (không phải chèn trình duyệt), bạn chỉ cần khóa trang web và pageurl - DOM bóng không thành vấn đề. Việc xử lý Shadow DOM chỉ cần thiết khi bạn phải trích xuất khóa trang web từ trang đang hoạt động hoặc đưa mã thông báo trở lại DOM.
Còn rễ bóng kín thì sao?
Rễ bóng đóng chặn truy cập el.shadowRoot. Cách giải quyết là ghi đè Element.prototype.attachShadow trước khi tải trang, buộc tất cả các gốc bóng phải mở chế độ. Tính năng này hoạt động trong Nhà viết kịch và Puppeteer thông qua page.evaluateOnNewDocument.
bài viết liên quan
- So sánh Geetest và Cloudflare Turnstile
- Cloudflare Turnstile 403 Sau khi sửa mã thông báo
- Giải thích về các chế độ widget Cloudflare Turnstile
Các bước tiếp theo
Xử lý CAPTCHA ẩn bên trong các thành phần web —lấy khóa API CaptchaAI của bạnvà thực hiện truyền tải bóng DOM.
Hướng dẫn liên quan: