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

Xử lý Shadow DOM CAPTCHA: Tiếp cận các phần tử trong Thành phần Web

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

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:

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