Tutorials

reCAPTCHA in Single-Page-Anwendungen: Dynamische Lademuster

Kurz gesagt: reCAPTCHA in einer Single-Page-Anwendung lösen Sie in vier Schritten:

  • auf das gerenderte Widget warten,
  • den Sitekey aus dem DOM oder dem JavaScript-Bundle auslesen,
  • das Token über CaptchaAI erzeugen lassen,
  • das Token dort einsetzen, wo React, Vue oder Angular es erwartet.

Die eigentliche Hürde ist das Timing, nicht das Lösen selbst: Das Widget steht nicht im ausgelieferten HTML, sondern taucht erst auf, wenn eine Komponente mountet, sich die Route ändert oder der Nutzer eine Aktion auslöst. Wer die Seitenquelle wie bei einer klassischen Seite ausliest, findet schlicht nichts. Dieser Leitfaden zeigt jeden Schritt konkret mit Playwright und Puppeteer.

Warum eine SPA eine Sonderbehandlung braucht

Bei einer klassischen serverseitig gerenderten Seite steht alles Nötige schon im HTML. Eine SPA baut die Oberfläche dagegen erst im Browser zusammen – und mit ihr das reCAPTCHA-Widget. Der Unterschied im Überblick:

Klassische Seite SPA
reCAPTCHA-Skript liegt im initialen HTML Skript wird nach dem Routenwechsel nachgeladen
Widget rendert beim Seitenaufruf Widget rendert beim Mount der Komponente
Sitekey steht in der Seitenquelle Sitekey steckt im JavaScript-Bundle
Formular wird per POST übermittelt Formular geht über XHR/fetch raus

In der Praxis begegnet Ihnen dieses Muster überall dort, wo moderne Frameworks im Einsatz sind:

  • ein React-Login, das das Widget erst beim Mount der Komponente rendert;
  • ein Vue-Checkout auf Shopware- oder JTL-Basis, der reCAPTCHA erst nach dem Klick auf „Bestellung prüfen“ nachlädt;
  • Behörden- und Visa-Terminportale (etwa BLS-Portale), die im DACH-Raum stark nachgefragt sind und das CAPTCHA erst nach dem Login einblenden.

Ihre Automatisierung darf das Widget also nicht sofort erwarten, sondern muss gezielt darauf warten.

Das reCAPTCHA-Widget erkennen

Auf das Element warten

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://example.com/login")

    # SPA may need a click or navigation to trigger reCAPTCHA
    page.click("#show-login-form")

    # Wait for reCAPTCHA iframe to appear
    recaptcha_frame = page.wait_for_selector(
        "iframe[src*='recaptcha']",
        timeout=15000
    )
    print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))

Den Sitekey extrahieren

In einer SPA kann der Sitekey an mehreren Stellen liegen. Prüfen Sie sie von der zuverlässigsten zur letzten Rückfalloption:

  1. im src-Attribut des reCAPTCHA-iframe (?k=…);
  2. im data-sitekey-Attribut des gerenderten Elements;
  3. als letzte Option im gebündelten JavaScript.

Die folgenden drei Methoden decken genau diese Fälle ab:

# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None

# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
    "[data-sitekey]",
    "el => el.getAttribute('data-sitekey')"
)

# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
    () => {
        // Check for grecaptcha render calls
        const scripts = document.querySelectorAll('script');
        for (const s of scripts) {
            const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
            if (match) return match[2];
        }
        return null;
    }
""")

Das reCAPTCHA lösen

Sobald der Sitekey vorliegt, übergeben Sie ihn an CaptchaAI. Der Solver braucht nur zwei Angaben:

  • den googlekey – also den extrahierten Sitekey;
  • die pageurl – die aktuelle Adresse der SPA-Ansicht.

Ob das Widget in einer SPA oder auf einer klassischen Seite sitzt, spielt für den Solver keine Rolle:

import requests
import time

def solve_recaptcha(site_key, page_url):
    # Submit task
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

Hinweis: Ein reCAPTCHA-Token ist nur rund 120 Sekunden gültig. Lösen Sie es deshalb erst kurz vor der Übermittlung und nicht auf Vorrat, sonst läuft es ab, bevor Ihr Zielaufruf es verwendet.

Das Token in die SPA einsetzen

Eine SPA sendet in der Regel kein klassisches Formular ab, sondern feuert einen XHR- oder fetch-Aufruf. Deshalb genügt es nicht, das Token irgendwo abzulegen – es muss genau dort landen, wo die Anwendung es beim Absenden ausliest. Drei Wege haben sich bewährt:

  1. das versteckte g-recaptcha-response-Textarea füllen;
  2. den registrierten Callback mit dem Token aufrufen;
  3. den XHR-Aufruf abfangen und das Token mitgeben.

Methode 1: Das versteckte Textarea füllen

token = solve_recaptcha(site_key, "https://example.com/login")

# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

# Submit the form
page.click("#login-button")

Methode 2: Den Callback auslösen

Viele reCAPTCHA-Einbindungen in SPAs arbeiten mit einer Callback-Funktion, die nach erfolgreichem Lösen aufgerufen wird. Diesen Callback rufen Sie mit dem Token direkt auf:

# Find the callback function name
callback = page.evaluate("""
    () => {
        const widget = document.querySelector('.g-recaptcha');
        return widget?.getAttribute('data-callback') || null;
    }
""")

# Call it with the token
if callback:
    page.evaluate(f"window['{callback}']('{token}')")
else:
    # Try the default grecaptcha callback
    page.evaluate(f"""
        if (window.grecaptcha) {{
            // Set the response and trigger verification
            document.getElementById('g-recaptcha-response').value = '{token}';
            // Find and call any registered callbacks
            const form = document.querySelector('form');
            if (form) form.dispatchEvent(new Event('submit'));
        }}
    """)

Methode 3: XHR abfangen (JavaScript / Puppeteer)

const puppeteer = require('puppeteer');

async function solveRecaptchaInSPA() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Intercept the API call and inject the token
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('/api/login')) {
      const postData = JSON.parse(request.postData() || '{}');
      // Token already set via page.evaluate — let it pass
      request.continue();
    } else {
      request.continue();
    }
  });

  await page.goto('https://example.com/login');
  await page.waitForSelector('[data-sitekey]');

  const siteKey = await page.$eval(
    '[data-sitekey]',
    el => el.getAttribute('data-sitekey')
  );

  // Mit CaptchaAI lösen (Implementierung ausgelassen, entspricht Python)
  const token = await solveCaptcha(siteKey, page.url());

  // Inject token and trigger callback
  await page.evaluate((t) => {
    document.getElementById('g-recaptcha-response').value = t;
    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && window[callback]) {
      window[callback](t);
    }
  }, token);
}

Framework-spezifische Muster

Jedes Framework rendert das Widget etwas anders:

  • React mountet die Komponente asynchron;
  • Vue blendet sie oft per v-if nach einer Interaktion ein;
  • Angular lädt reCAPTCHA-Module häufig verzögert (lazy) nach.

Die folgenden Snippets treffen diese typischen Fälle.

React (react-google-recaptcha)

React-Apps setzen häufig auf das Paket react-google-recaptcha. Die Komponente rendert asynchron, deshalb warten Sie auf das gemountete Element, bevor Sie den Sitekey lesen:

# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")

# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
    ".g-recaptcha", "el => el.dataset.sitekey"
)

Vue (vue-recaptcha)

Vue blendet das Widget oft per v-if erst nach einer Interaktion ein – lösen Sie die Bedingung aus und warten Sie dann auf den iframe:

# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")

Angular

# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")

Routenwechsel abfangen

In einer SPA wird dieselbe Seite wiederverwendet: Die Navigation läuft ohne vollständiges Neuladen. Daraus folgen zwei Punkte für Ihre Automatisierung:

  • Das reCAPTCHA erscheint mitten im Lebenszyklus, ohne dass ein neuer Seitenaufruf stattfindet.
  • Ein wait_for_selector auf den iframe ist zuverlässiger als eine feste Wartezeit.

Warten Sie nach dem Routenwechsel also gezielt auf das Widget:

# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")

# Navigate within the SPA
page.click("a[href='/login']")

# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)

Fehlerbehebung

Problem Ursache Lösung
g-recaptcha-response wird nicht gefunden Die SPA hat das Widget noch nicht gerendert Warten Sie mit wait_for_selector und ausreichend Timeout auf das Element
Token wird erzeugt, aber vom Ziel abgelehnt sitekey, pageurl oder Session-Kontext passen nicht zusammen Erfassen Sie die Parameter erneut und verwenden Sie das Token in derselben Browser- oder HTTP-Sitzung
Token ist nach dem Einfügen wieder leer React/Vue rendert die Komponente neu und leert das Feld Setzen Sie das Token unmittelbar vor dem Absenden oder nutzen Sie den data-callback
Polling läuft in ein Timeout Intervall, Wartezeit oder Fehlerbehandlung sind zu eng gesetzt Fragen Sie alle 5–10 Sekunden ab, trennen Sie das Timeout von echten Fehlercodes und protokollieren Sie die Ursache
Läuft lokal, aber nicht im Workflow Callback, Formularfeld oder Token-Übergabe fehlt in der echten Zielkette Prüfen Sie den exakten Übergabepfad vom Solver bis zur finalen Zielanfrage

Häufige Fragen

Wie finde ich den Sitekey, wenn er nur im JavaScript-Bundle steckt?

Warten Sie zuerst, bis das Framework das Widget gerendert hat, und lesen Sie den Sitekey dann aus der iframe-src (?k=…) oder dem data-sitekey-Attribut. Das Durchsuchen des gebündelten JavaScripts ist die letzte Rückfalloption – meist ist der Wert nach dem Rendern längst im DOM verfügbar.

Warum ist mein Token nach dem Einfügen plötzlich wieder leer?

Weil die Reaktivität von React oder Vue die Komponente neu rendert und das Feld dabei überschreibt. Setzen Sie das Token unmittelbar vor dem Absenden oder rufen Sie den data-callback direkt auf, statt nur das versteckte Textarea zu füllen.

Gilt derselbe Ablauf auch für reCAPTCHA v3 in einer SPA?

Im Kern ja: Sitekey und Page-URL bleiben die Grundlage. reCAPTCHA v3 liefert allerdings einen Score im Hintergrund statt einer Checkbox, sodass das Token oft schon beim Laden angefordert und an denselben API-Aufruf gehängt wird.

Was kostet das Lösen von reCAPTCHA bei CaptchaAI?

CaptchaAI rechnet pro Thread ab, nicht pro Lösung. Der Einstieg BASIC kostet 15 $/Monat mit 5 Threads und unbegrenzten Lösungen pro Thread – zusätzliche Gebühren pro CAPTCHA oder je nach Typ fallen nicht an.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.