Tutorials

CAPTCHA-Handhabung in Progressive Web Apps (PWAs)

Progressive Web-Apps stellen einzigartige CAPTCHA-Herausforderungen dar. Sie verwenden clientseitiges Rendering, Service Worker und Einzelseitennavigation – das bedeutet, dass CAPTCHAs dynamisch und nicht mit dem ursprünglichen HTML geladen werden. CaptchaAI übernimmt die Lösung, aber Sie benötigen die richtige Erkennungsstrategie, um CAPTCHAs abzufangen, die nach dem Laden der Seite in das DOM eingefügt werden.

Dieser Leitfaden behandelt das Erkennen, Extrahieren und Lösen von CAPTCHAs in PWA-Kontexten mit Playwright und CaptchaAI.

Woran erkennen Sie, dass das Problem eher die PWA-Struktur ist?

Beobachtung Wahrscheinlichere Ursache
Im ersten HTML ist kein CAPTCHA sichtbar, später aber schon Das Widget wird erst clientseitig gerendert
Nach Navigation innerhalb der App erscheint plötzlich ein neues CAPTCHA Routing läuft ohne echten Page-Reload
Dasselbe Widget verhält sich nach Updates inkonsistent Service Worker oder Cache liefern veraltete Ressourcen
Sitekey-Extraktion klappt nur manchmal Der DOM-Zeitpunkt stimmt nicht mit dem Framework-Lifecycle überein

Warum PWAs anders sind

Herkömmliche Websites stellen CAPTCHAs in der ersten HTML-Antwort bereit. PWAs unterscheiden sich in mehreren wesentlichen Punkten:

Aspekt Traditionelle Seite PWA
CAPTCHA wird geladen Im anfänglichen HTML Nach dem Laden der Seite von JavaScript gerendert
Seitennavigation Komplette Seite neu laden Clientseitiges Routing (kein Neuladen)
Servicemitarbeiter Nicht vorhanden Speichert Ressourcen im Cache und kann Anfragen abfangen
DOM-Verfügbarkeit Sofort Nach dem Rendern des Frameworks
Netzwerkanfragen Direkt Kann vom Servicemitarbeiter abgefangen werden

Schritt 1: Warten Sie auf das dynamische CAPTCHA-Rendering

Der größte Fehler besteht darin, Sitekeys zu extrahieren, bevor das PWA-Framework das CAPTCHA-Widget gerendert hat. Verwenden Sie Mutationsbeobachter oder Framework-spezifische Signale:

// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function detectCaptchaInPWA(page) {
  // Wait for the PWA app shell to render
  await page.waitForLoadState('networkidle');

  // Use MutationObserver to detect dynamically loaded CAPTCHAs
  const captchaInfo = await page.evaluate(() => {
    return new Promise((resolve) => {
      // Check if CAPTCHA is already present
      const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
      if (existing) {
        resolve({
          type: existing.classList.contains('g-recaptcha')
            ? 'recaptcha_v2' : 'turnstile',
          sitekey: existing.getAttribute('data-sitekey'),
          pageurl: window.location.href,
        });
        return;
      }

      // Watch for CAPTCHA elements added dynamically
      const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeType !== 1) continue;
            const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
              ? node
              : node.querySelector?.('.g-recaptcha, .cf-turnstile');
            if (captcha) {
              observer.disconnect();
              resolve({
                type: captcha.classList.contains('g-recaptcha')
                  ? 'recaptcha_v2' : 'turnstile',
                sitekey: captcha.getAttribute('data-sitekey'),
                pageurl: window.location.href,
              });
              return;
            }
          }
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      // Timeout after 15 seconds
      setTimeout(() => {
        observer.disconnect();
        resolve(null);
      }, 15000);
    });
  });

  return captchaInfo;
}

async function main() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example-pwa.com/login');

  const captcha = await detectCaptchaInPWA(page);

  if (!captcha) {
    console.log('No CAPTCHA detected');
    await browser.close();
    return;
  }

  console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);

  // Mit CaptchaAI lösen
  const token = await solveCaptcha(captcha);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Inject token
  await injectToken(page, captcha.type, token);

  // Submit form
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle' });

  console.log('Form submitted');
  await browser.close();
}

async function solveCaptcha(captcha) {
  const params = {
    key: API_KEY,
    pageurl: captcha.pageurl,
    json: '1',
  };

  if (captcha.type === 'recaptcha_v2') {
    params.method = 'userrecaptcha';
    params.googlekey = captcha.sitekey;
  } else {
    params.method = 'turnstile';
    params.sitekey = captcha.sitekey;
  }

  const submit = await axios.get(
    'https://ocr.captchaai.com/in.php', { params }
  );
  if (submit.data.status !== 1) throw new Error(submit.data.request);

  const taskId = submit.data.request;

  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      throw new Error(poll.data.request);
    }
  }
  throw new Error('Timeout');
}

async function injectToken(page, type, token) {
  if (type === 'recaptcha_v2') {
    await page.evaluate((t) => {
      document.getElementById('g-recaptcha-response').value = t;
      try {
        const clients = ___grecaptcha_cfg.clients;
        Object.keys(clients).forEach((k) => {
          Object.keys(clients[k]).forEach((j) => {
            if (clients[k][j]?.callback) clients[k][j].callback(t);
          });
        });
      } catch (e) {}
    }, token);
  } else {
    await page.evaluate((t) => {
      const input = document.querySelector('[name="cf-turnstile-response"]');
      if (input) input.value = t;
      const cb = document.querySelector('.cf-turnstile')
        ?.getAttribute('data-callback');
      if (cb && typeof window[cb] === 'function') window[cb](t);
    }, token);
  }
}

main().catch(console.error);

Schritt 2: Behandeln Sie das Service-Worker-Caching

Servicemitarbeiter können CAPTCHA-Skripte zwischenspeichern, was zu veralteten Widgets führt. Umgehen Sie den Cache bei Bedarf:

// Intercept and bypass Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

await page.route('**/turnstile/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

Schritt 3: Behandeln Sie die clientseitige Navigation

PWAs verwenden clientseitiges Routing – das Navigieren zu einer CAPTCHA-geschützten Route löst keinen Seitenladevorgang aus. Überwachen Sie Routenänderungen:

// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
  const originalPushState = history.pushState;
  history.pushState = function() {
    originalPushState.apply(this, arguments);
    window.dispatchEvent(new Event('pwa-route-change'));
  };
});

page.on('console', async (msg) => {
  // React to route changes if needed
});

// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);

Schritt 4: Python-Alternative mit Selenium

# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

API_KEY = "YOUR_API_KEY"

driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")

# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)

sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")

# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
    params["method"] = "turnstile"
    params["sitekey"] = sitekey
else:
    params["method"] = "userrecaptcha"
    params["googlekey"] = sitekey

resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]

# Poll
for _ in range(30):
    time.sleep(5)
    poll = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1",
    })
    if poll.json().get("status") == 1:
        token = poll.json()["request"]
        break
else:
    raise TimeoutError("CAPTCHA not solved")

# Inject token
driver.execute_script(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()

Fehlerbehebung

Problem Ursache Lösung
Token wird erzeugt, aber vom Ziel abgelehnt sitekey, pageurl oder Session-Kontext stimmen nicht Erfasse Parameter erneut und verwende den Token in derselben Browser- oder HTTP-Sitzung
Polling endet im Timeout Intervall, Wartezeit oder Fehlerbehandlung sind zu eng gesetzt Poll alle 5-10 Sekunden, trenne Timeout von echten Fehlercodes und logge die Ursache
Beispiel funktioniert lokal, aber nicht im Workflow Callback, Form-Feld oder Token-Injektion fehlt in der echten Zielkette Prüfe den exakten Übergabepfad vom Solver bis zur finalen Zielanfrage

Verwandte Leitfäden

  • Shadow DOM CAPTCHA-Verwaltung in Web Components
  • React Native WebView CAPTCHA-Lösung
  • Mobile Browser-Automatisierung mit CAPTCHA
Kommentare sind für diesen Artikel deaktiviert.