Eğitimler

Shadow DOM CAPTCHA İşleme: Web Bileşenlerindeki Öğelere Ulaşma

Standart document.querySelector gölge köklerin içine ulaşamaz. Bir CAPTCHA widget'ı bir web bileşeninin gölge DOM'unda oluşturulduğunda, her zamanki site anahtarı çıkarma komut dosyanız null değerini döndürür. CAPTCHA orada; seçiciniz onu göremiyor.

CAPTCHA'lar Neden Gölge DOM'da Sonlanıyor?

Senaryo Sebep
Özel oturum açma bileşeni Yeniden kullanılabilir bir web bileşeni olarak kapsüllenmiştir
Üçüncü taraf form widget'ı Widget satıcısı tüm formu gölge köküne sarıyor
Mikro ön uç mimarisi Her mikro uygulama izole edilmiş gölge DOM kullanır
Tasarım sistemi bileşenleri CAPTCHA bileşen kitaplığı öğesine gömülü

Gölge DOM CAPTCHA'larını Algılama

Çözmeden önce CAPTCHA'nın gölge kökün içinde olduğunu doğrulayın:

// 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: Oyun Yazarı Gölge DOM Delici

Oyun Yazarının >> delici seçicisi ve locator API'si gölge DOM'u yerel olarak yönetir:

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: Puppeteer Gölge DOM Geçişi

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();
})();

Gölge DOM Derinliğiyle İlgili Hususlar

Derinlik Örnek Yaklaşım
1 seviye <custom-form> #shadow-root > .cf-turnstile Doğrudan gölge kök sorgusu
2+ seviye <app-shell> #shadow > <login-form> #shadow > .cf-turnstile Özyinelemeli geçiş
Gölge kökünü aç el.shadowRoot erişilebilir Standart yaklaşım – yinelemeyi kullanın
Kapalı gölge kökü el.shadowRoot null değerini döndürür Delinemez; page.evaluate'yi attachShadow({mode:'open'}) geçersiz kılma veya engelleme oluşturma ile birlikte kullanın

Geri dönüş tanılama sırası

  • Önce açık gölge köklerini, ardından iframe sınırlarını ve yalnızca uygulama düzeyindeki geri aramaları veya enjekte edilen kancaları kontrol edin.
  • Gelecekteki hedef değişikliklerinin tespit edilmesinin daha kolay olması için hangi katmanın zorluğa maruz kaldığını kaydedin.
  • Her sayfanın DOM erişimini eşit şekilde gösterdiğini varsaymak yerine, kapalı kökler için son çare olarak bir yedek bulundurun.

Sorun giderme

Sorun Sebep Düzeltme
shadowRoot, null'yi döndürür Kapalı gölge DOM mode: 'open''yi zorlamak için sayfa yüklenmeden önce attachShadow'yi geçersiz kılın
Site anahtarı bulundu ancak jeton reddedildi Çözücüye yanlış sayfa URL'si gönderildi Çıkarma sırasında sabit kodlanmış bir URL değil page.url() kullanın
Jeton enjekte edildi ancak form gönderilmiyor CAPTCHA geri araması tetiklenmedi Belirteç değerini ayarladıktan sonra geri arama işlevini bulun ve çağırın
Özyinelemeli arama yavaş Derin DOM ağacı Özyineleme derinliğini sınırlayın; bilinen gölge ana makine etiketi adlarını hedefle
CAPTCHA ilk yüklemede bulunamadı Widget eşzamansız olarak yükleniyor Önce gölge ana bilgisayar öğesini bekleyin: page.waitForSelector('custom-form')

SSS

CAPTCHA'nın gölge DOM kullanıp kullanmadığını nasıl anlarım?

Chrome DevTools'ta CAPTCHA öğesini inceleyin. CAPTCHA işaretlemesinin üzerinde #shadow-root (open) veya #shadow-root (closed) görüyorsanız, bu bir gölge kökün içindedir. document'den normal querySelector onu bulamayacak.

Gölge DOM geçişini tamamen önleyebilir miyim?

CaptchaAI API'sini doğrudan kullanıyorsanız (tarayıcı enjeksiyonu değil), yalnızca site anahtarına ve sayfa URL'sine ihtiyacınız vardır; gölge DOM önemli değildir. Gölge DOM işleme yalnızca site anahtarlarını canlı sayfadan çıkarmanız veya belirteçleri DOM'a geri eklemeniz gerektiğinde gereklidir.

Peki ya kapalı gölge kökleri?

Kapalı gölge kökleri el.shadowRoot erişimini engeller. Çözüm, sayfa yüklenmeden önce Element.prototype.attachShadow'yi geçersiz kılmak ve tüm gölge kökleri açık moda zorlamaktır. Bu, Oyun Yazarı ve Puppeteer'da page.evaluateOnNewDocument aracılığıyla çalışır.

İlgili Makaleler

Sonraki Adımlar

Web bileşenlerinin içine gizlenmiş CAPTCHA'ları yönetin -CaptchaAI API anahtarınızı alınve gölge DOM geçişini uygulayın.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.