reCAPTCHA in WebView2 mit C# lösen (WinForms, WPF)

solve recaptcha in webview2 - How to Solve reCAPTCHA in WebView2 With C# (WinForms, WPF)

Um reCAPTCHA in WebView2 zu lösen, warten Sie auf NavigationCompleted, lesen den sitekey mit ExecuteScriptAsync aus und senden ihn mit einem einfachen await an CapSkip, damit Ihr Code auf den UI-Thread zurückkehrt. Schreiben Sie dann den Token in jedes Feld g-recaptcha-response und rufen Sie den Callback der Seite auf oder senden Sie das Formular ab. Zwei Details verursachen die meisten Fehlschläge. ExecuteScriptAsync gibt JSON zurück, keinen String, daher kommt der sitekey in Anführungszeichen verpackt an. Und ein einziges ConfigureAwait(false) beim Lösevorgang lässt den nächsten WebView2-Aufruf eine InvalidOperationException auslösen, weil CoreWebView2 nur auf dem Thread antwortet, der es erstellt hat. Dieser Leitfaden baut eine kleine WinForms-App, die beides behandelt, dazu unsichtbare Widgets, explizit gerenderte Widgets und das Schließen des Fensters mitten im Lösevorgang.

Was Sie brauchen

  • CapSkip auf einem Windows-Rechner, mit eingeschaltetem reCAPTCHA-Löser.
  • Das .NET 8 SDK oder neuer. Der Code unten wurde unter .NET 8 und .NET 10 ausgeführt, gebaut mit dem SDK 10.0.100.
  • Die WebView2 Runtime. Windows 11 bringt sie mit, und sie aktualisiert sich automatisch, unabhängig vom Edge-Browser.
  • Die Pakete Microsoft.Web.WebView2 und CapSkip von NuGet.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit eine App auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.
dotnet new winforms -n WvApp
cd WvApp
dotnet add package Microsoft.Web.WebView2
dotnet add package CapSkip

Für net8.0-windows und net10.0-windows läuft der Restore beider Pakete fehlerfrei durch. Ein WinForms-Build gibt eine harmlose Warnung MSB3277 zu widersprüchlichen Versionen von WindowsBase aus, weil das WebView2-Paket immer auch seine WPF-Assembly referenziert. Sie beeinträchtigt die App nicht, und ein WPF-Projekt zeigt sie nie. Wenn Sie UseWPF auf true setzen, verschwindet sie ebenfalls, aber dann fällt System.IO aus den impliziten usings heraus, und Path.Combine lässt sich nicht mehr kompilieren.

Schritt 1: WebView2 einen beschreibbaren Datenordner geben

WebView2 speichert Cookies, Cache und lokalen Speicher in einem Benutzerdatenordner. Wenn Sie keinen angeben, legt es ihn neben Ihrer exe an, als WvApp.exe.WebView2. Das funktioniert, solange Sie die App aus bin\Debug starten. Sobald die App an einem Ort installiert ist, an dem ein Standardbenutzer nicht schreiben darf, etwa unter Program Files, scheitert die Initialisierung sofort mit einer UnauthorizedAccessException (Access is denied). Geben Sie stattdessen einen Ordner unter den lokalen App-Daten des Benutzers an.

async Task StartAsync()
{
    var dataDir = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
    var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
    await webView.EnsureCoreWebView2Async(env);
    webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
    webView.CoreWebView2.Navigate(LoginUrl);
}

Erstellen Sie die Umgebung, bevor irgendetwas anderes das Steuerelement anfasst. Wenn Sie zuerst die Eigenschaft Source setzen, startet die Initialisierung mit der Standardumgebung, und ein späteres EnsureCoreWebView2Async mit Ihrer eigenen Umgebung löst dann eine ArgumentException aus, laut der WebView2 bereits mit einer anderen CoreWebView2Environment initialisiert wurde. Auch ein Aufruf von ExecuteScriptAsync auf dem Steuerelement vor der Initialisierung scheitert, mit einer InvalidOperationException, die Sie auf EnsureCoreWebView2Async verweist. Über webView.CoreWebView2, wie im Code dieses Leitfadens, führt derselbe Fehler zu einer NullReferenceException, weil CoreWebView2 noch null ist. Microsofts Seite zum WebView2-Benutzerdatenordner erklärt, wo der Ordner liegen darf.

Schritt 2: Den sitekey auslesen und dekodieren, was ExecuteScriptAsync zurückgibt

Der sitekey steht in der Seite, also lesen Sie ihn mit JavaScript aus. Ein Checkbox-Widget oder ein unsichtbares Widget trägt ihn in einem Attribut data-sitekey. Eine Seite, die das Widget selbst mit grecaptcha.render rendert, hat kein solches Attribut, aber das Anchor-iframe von Google trägt denselben Schlüssel in seinem Query-Parameter k, und darauf greift das Skript dann zurück.

// data-sitekey first, then the anchor iframe that an explicit render leaves
// behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
const string FindSitekeyJs = """
    (() => {
      const el = document.querySelector('.g-recaptcha[data-sitekey]');
      if (el) return el.getAttribute('data-sitekey');
      const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
      return f ? new URL(f.src).searchParams.get('k') : null;
    })()
    """;

async Task<string?> EvalStringAsync(string js) =>
    JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));

ExecuteScriptAsync gibt das Ergebnis des Skripts als JSON serialisiert zurück. Ein sitekey kommt mit seinen Anführungszeichen an, und ein fehlendes Element kommt als die vier Buchstaben null an. Ein Skript, das eine Exception wirft, zum Beispiel einen TypeError bei einem Element, das nicht existiert, gibt ebenfalls null zurück, ohne Exception auf der C#-Seite. Wenn Sie mit System.Text.Json deserialisieren, erhalten Sie einen echten String oder ein echtes null. Die Anführungszeichen von Hand abzuschneiden sieht genauso aus, bis ein Wert ein escaptes Zeichen enthält, und dann geht es kaputt.

Führen Sie das nach NavigationCompleted aus, nicht nach DOMContentLoaded. Googles api.js lädt asynchron, daher existiert bei DOMContentLoaded zwar das div des Widgets, aber die textarea für die Antwort, das Objekt grecaptcha und das Anchor-iframe womöglich noch nicht. Beim ersten Besuch erschienen sie in unseren Durchläufen 0,3 bis 1,4 Sekunden später, immer vor NavigationCompleted. Sobald das Skript im Cache lag, waren sie bei DOMContentLoaded schon da. Code, der zu früh liest, kann also auf Ihrem Rechner funktionieren und bei einem neuen Benutzer scheitern.

Schritt 3: Das Captcha lösen, ohne den UI-Thread zu verlassen

An dieser Regel scheitern die meisten Versuche, reCAPTCHA in WebView2 zu lösen. CoreWebView2 gehört zum UI-Thread, der es erstellt hat, und jeder Zugriff von einem anderen Thread aus löst eine Exception aus. Leitfäden für Bibliothekscode raten, an jedes await ein ConfigureAwait(false) anzuhängen. In einem WebView2-Event-Handler ist dieser Rat falsch.

// In OnNavigationCompleted, where core is webView.CoreWebView2.
// Throws: ConfigureAwait(false) resumes on a thread-pool thread
var result = await solver.RecaptchaAsync(sitekey, core.Source).ConfigureAwait(false);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");
// System.InvalidOperationException: CoreWebView2 can only be accessed from the UI thread.

Mit ConfigureAwait(false) kann der Code nach dem Lösevorgang auf einem Thread-Pool-Thread weiterlaufen. Der nächste Aufruf von CoreWebView2 scheitert dann mit genau dieser InvalidOperationException. Dasselbe passiert, wenn Sie den Skriptaufruf in Task.Run verpacken. In WPF lautet die Meldung anders: The calling thread cannot access this object because a different thread owns it. Entkommt die Exception einem async void-Handler, stürzt die App ab. Die vollständige Regel lesen Sie in Microsofts Hinweisen zum WebView2-Threadingmodell nach.

// Works: a plain await inside a WinForms event handler comes back on the UI
// thread
var result = await solver.RecaptchaAsync(sitekey, core.Source);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");

Ein einfaches await in einem WinForms- oder WPF-Event-Handler erfasst den Synchronisierungskontext der UI, sodass der Code danach wieder auf dem UI-Thread läuft. Die UI bleibt reaktionsfähig, während CapSkip arbeitet: Ein Timer mit 100 Millisekunden tickte in unserem Test 71 Mal während eines Lösevorgangs von 7,88 Sekunden. Blockieren mit GetAwaiter().GetResult() führt nicht zu einem Deadlock, weil das CapSkip SDK intern ConfigureAwait(false) verwendet, aber es friert das Fenster für den gesamten Lösevorgang ein.

Schritt 4: Den Token einsetzen und das Formular absenden

Sobald CapSkip zurückkehrt, setzen Sie den Token dort ein, wo das Skript von Google ihn eingesetzt hätte, und lösen dann aus, was die Seite als Nächstes tut.

// fill every response textarea, then call data-callback or submit the form
const string InjectJs = """
    (token => {
      const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
      boxes.forEach(b => { b.value = token; });
      const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
      if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
      const form = boxes[0]?.form;
      if (!form) return 'no form found';
      if (!form.checkValidity()) return 'form has empty required fields';
      form.requestSubmit();
      return 'submitted the form';
    })
    """;

Füllen Sie jede textarea g-recaptcha-response, nicht nur die erste. Ein Formular mit zwei Widgets oder mit einer eigenen textarea neben der von Google hat zwei Felder mit diesem Namen, und ein Server wie PHP behält nur das letzte. Auf unserer Testseite mit zwei Feldern wurde das Setzen nur des ersten Felds abgelehnt, das Setzen beider Felder dagegen akzeptiert.

Ein Widget mit einem Attribut data-callback erwartet, dass der Callback der Seite mit dem Token läuft, also ruft das Skript ihn auf. Auf unserer Testseite mit Callback wurde das Absenden des Formulars ohne den Callback abgelehnt. Eine Seite, die das Widget mit grecaptcha.render rendert, übergibt ihren Callback stattdessen in JavaScript, wo das Skript ihn nicht sehen kann. Suchen Sie den Funktionsnamen im eigenen Skript der Seite und rufen Sie die Funktion auf dieselbe Weise mit dem Token auf.

Andernfalls sendet das Skript das Formular mit form.requestSubmit() ab, das die eigenen Submit-Handler der Seite und die HTML5-Validierung ausführt, wie ein Klick auf ihren Button. Ein Unterschied bleibt: Name und Wert des Buttons selbst werden nur gesendet, wenn Sie den Button übergeben, wie in form.requestSubmit(button). Die ältere Methode form.submit() überspringt die Handler und die Validierung, sodass ein leer gelassenes Pflichtfeld trotzdem an den Server geht. Die Zeile mit checkValidity meldet diesen Fall, statt ein halb leeres Formular abzusenden.

Der Token wird als JSON-Stringliteral übergeben, erzeugt mit JsonSerializer.Serialize, sodass Anführungszeichen oder Backslashes darin das Skript nie kaputt machen können. Setzen Sie ihn sofort ein. Google dokumentiert, dass ein reCAPTCHA-Token zwei Minuten nach seiner Ausgabe abläuft.

Das vollständige WinForms-Beispiel

Erstellen Sie das Projekt mit dem Namen WvApp, damit diese Datei einen Namespace mit der Form1.Designer.cs teilt, die die Vorlage angelegt hat. Ersetzen Sie Form1.cs durch den Code unten, ändern Sie LoginUrl auf Ihre Seite und starten Sie die App mit dotnet run. Program.cs bleibt so, wie die Vorlage es geschrieben hat.

// Form1.cs, in a project made with: dotnet new winforms -n WvApp
using System.Text.Json;
using CapSkip;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;

namespace WvApp;

public partial class Form1 : Form
{
    const string LoginUrl = "https://example.com/login";

    // data-sitekey first, then the anchor iframe that an explicit render leaves
    // behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
    const string FindSitekeyJs = """
        (() => {
          const el = document.querySelector('.g-recaptcha[data-sitekey]');
          if (el) return el.getAttribute('data-sitekey');
          const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
          return f ? new URL(f.src).searchParams.get('k') : null;
        })()
        """;

    const string IsInvisibleJs = """
        (() => {
          const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
          if (f) return new URL(f.src).searchParams.get('size') === 'invisible';
          const el = document.querySelector('.g-recaptcha[data-sitekey]');
          if (!el) return false;
          if (el.getAttribute('data-size') === 'invisible') return true;
          return el.tagName === 'BUTTON' && !el.hasAttribute('data-action');
        })()
        """;

    // fill every response textarea, then call data-callback or submit the form
    const string InjectJs = """
        (token => {
          const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
          boxes.forEach(b => { b.value = token; });
          const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
          if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
          const form = boxes[0]?.form;
          if (!form) return 'no form found';
          if (!form.checkValidity()) return 'form has empty required fields';
          form.requestSubmit();
          return 'submitted the form';
        })
        """;

    readonly WebView2 webView = new() { Dock = DockStyle.Fill };
    readonly CapSkipClient solver = new(host: "127.0.0.1", port: 8080);
    readonly CancellationTokenSource cts = new();

    public Form1()
    {
        InitializeComponent();
        Controls.Add(webView);
        Load += async (_, _) => await StartAsync();
        FormClosing += (_, _) => cts.Cancel();
    }

    async Task StartAsync()
    {
        var dataDir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
        var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
        await webView.EnsureCoreWebView2Async(env);
        webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
        webView.CoreWebView2.Navigate(LoginUrl);
    }

    async Task<string?> EvalStringAsync(string js) =>
        JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));

    async void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
    {
        var core = webView.CoreWebView2;
        var sitekey = await EvalStringAsync(FindSitekeyJs);
        if (sitekey is null) return;                      // no widget on this page
        core.NavigationCompleted -= OnNavigationCompleted; // solve once, not on the result page
        var invisible = await core.ExecuteScriptAsync(IsInvisibleJs) == "true";
        var options = invisible ? new Dictionary<string, object?> { ["invisible"] = 1 } : null;
        Text = "Solving reCAPTCHA...";
        try
        {
            var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
            var token = JsonSerializer.Serialize(result.Code);
            Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
        }
        catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
        catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }
    }
}

Der Handler entfernt sich selbst, sobald er einen sitekey findet. NavigationCompleted feuert erneut für die Seite, an die das Formular sendet, und ohne diese Zeile würde die App versuchen, auch die Ergebnisseite zu lösen. Auf einer Seite ohne Widget kehrt der Handler früh zurück und bleibt angehängt, sodass er nach der nächsten Navigation erneut prüft. Ein Widget, das später auf derselben Seite erscheint, braucht die Polling-Schleife, die weiter unten gezeigt wird. Ein unsichtbares Widget wird am Parameter size des Anchor-iframes von Google erkannt. Solange dieses iframe noch nicht existiert, verrät es ein Attribut data-size oder ein an einen Button gebundenes Widget. Es wird dann mit der Option invisible an CapSkip gesendet. Der Fenstertitel zeigt den Status an. Für ein Tool reicht das, und stattdessen kann auch ein Label ihn anzeigen.

Die CancellationTokenSource wird abgebrochen, sobald das Fenster sich zu schließen beginnt. Schließen Sie die App mitten in einem Lösevorgang, hört das SDK sofort mit einer TaskCanceledException auf zu pollen, die der erste catch-Block schluckt. Ohne den Token läuft ein Lösevorgang auf einem sekundären Formular weiter, nachdem das Formular schon geschlossen ist. Wenn er zurückkehrt, ist CoreWebView2 null, und der nächste Skriptaufruf löst eine NullReferenceException aus, die WinForms in seinem Dialog für unbehandelte Exceptions anzeigt.

Derselbe Code in WPF

Um reCAPTCHA in WebView2 unter WPF zu lösen, behalten Sie die Schritte oben bei. Das WPF-Steuerelement hat darunter dasselbe CoreWebView2, daher ändern sich die Aufrufe an WebView2 und CapSkip nicht. Deklarieren Sie das Steuerelement in MainWindow.xaml mit dem Namespace Microsoft.Web.WebView2.Wpf und nennen Sie es webView. Entfernen Sie dann das Feld webView und die Zeile mit Controls.Add. Verschieben Sie den Rest in MainWindow.xaml.cs, starten Sie ihn aus dem Event Loaded und brechen Sie den Token im Event-Handler für Closing ab. Ein WPF-Fenster hat keine Eigenschaft Text, schreiben Sie den Status also stattdessen in Title. Die impliziten usings von WPF lassen System.IO weg, daher braucht Path.Combine eine eigene using-Zeile. Entfernen Sie in dieser Datei den Import von System.Windows.Shapes aus der Vorlage, denn dieser Namespace hat eine eigene Klasse Path, und der Name wird mehrdeutig.

Widgets in iframes und Widgets, die spät laden

ExecuteScriptAsync läuft nur im obersten Dokument. Wenn das Login-Formular selbst in einem iframe sitzt, gibt das sitekey-Skript dort null zurück. Sammeln Sie die Frames der Seite mit dem Event FrameCreated, das Sie vor der Navigation verdrahten müssen, und verwerfen Sie jeden Frame, wenn er zerstört wird. Ein Frame, der nicht mehr existiert, löst beim Aufruf eine InvalidOperationException aus.

// a field: the page's iframes, filled before Navigate() and pruned as they go
readonly List<CoreWebView2Frame> frames = new();

// in StartAsync, before Navigate():
webView.CoreWebView2.FrameCreated += (_, e) =>
{
    var frame = e.Frame; // read it once: each read returns a new wrapper
    frames.Add(frame);
    frame.Destroyed += (_, _) => frames.Remove(frame);
};

Jeder CoreWebView2Frame hat sein eigenes ExecuteScriptAsync, daher laufen dieselben Skripte im Frame. Senden Sie CapSkip die eigene Adresse des Frames als Seiten-URL, denn das ist die Seite, auf der das Widget lebt. Rufen Sie diese Methode aus OnNavigationCompleted auf, wenn das oberste Dokument keinen sitekey liefert, innerhalb derselben try- und catch-Blöcke wie beim eigentlichen Lösevorgang. FrameCreated feuert auch für die eigenen Anchor- und Challenge-iframes von Google, und die Schleife überspringt sie, weil das Skript darin null zurückgibt.

// widget inside a cross-origin iframe: run the same scripts in that frame
async Task SolveInFramesAsync()
{
    foreach (var frame in frames.ToList())
    {
        var key = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync(FindSitekeyJs));
        if (key is null) continue;
        var href = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync("location.href"))!;
        var result = await solver.RecaptchaAsync(key, href, null, cts.Token);
        var token = JsonSerializer.Serialize(result.Code);
        await frame.ExecuteScriptAsync($"({InjectJs})({token})");
        return;
    }
}

Manche Seiten fügen das Widget erst nach dem load-Event hinzu, zum Beispiel nach einem Cookie-Banner oder einem ersten Klick. NavigationCompleted findet dann nichts. Eine kurze Polling-Schleife löst das: Auf einer Seite, die ihr Widget zwei Sekunden nach dem Laden hinzufügte, fand sie den Schlüssel nach 2,11 Sekunden.

// for pages that add the widget after the load event
async Task<string?> WaitForSitekeyAsync(TimeSpan timeout)
{
    var deadline = DateTime.UtcNow + timeout;
    while (DateTime.UtcNow < deadline)
    {
        var key = await EvalStringAsync(FindSitekeyJs);
        if (key is not null) return key;
        await Task.Delay(250);
    }
    return null;
}

CapSkip auf einem anderen Rechner betreiben

Die App und der Solver müssen sich keinen PC teilen. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP. Richten Sie den Client anschließend auf diese Adresse.

// CapSkip in Server mode on another Windows machine on your network
var solver = new CapSkipClient(apiKey: "YOUR_API_KEY", host: "192.168.1.50", port: 8080);

Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels, mit demselben Schlüssel im Client. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um ihn auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Auf demselben Rechner schreiben Sie 127.0.0.1 statt localhost. In unseren Durchläufen versuchte localhost zuerst die IPv6-Adresse und wartete bei jeder neuen Verbindung etwa zwei Sekunden, bis dieser Versuch scheiterte. Bei einem Solver, der nicht lief, kam die Fehlermeldung deshalb erst nach vier statt nach zwei Sekunden.

Fehler von CapSkip in der App behandeln

Das SDK löst vier Exceptions aus, die alle von CapSkipError abgeleitet sind. Um für jeden Fehlschlag eine eigene Meldung anzuzeigen, ersetzen Sie den try-Block in OnNavigationCompleted durch diesen. Lassen Sie den Filter für den Abbruch an erster Stelle: Ein Fenster, das geschlossen wird, während die Aufgabe gesendet wird, löst eine NetworkException aus, und ein späterer Filter würde sie als Fehler anzeigen.

try
{
    var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
    var token = JsonSerializer.Serialize(result.Code);
    Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
}
catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
catch (NetworkException ex) { Text = "Is CapSkip running? " + ex.Message; }
catch (ApiException ex) { Text = "CapSkip could not solve it: " + ex.Message; }
catch (CapSkip.TimeoutException) { Text = "No answer before the timeout"; }
catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }

Bei einem Namen ist Vorsicht geboten. Mit den impliziten usings eines WinForms-Projekts lässt sich ein bloßes catch von TimeoutException nicht kompilieren und scheitert mit CS0104, weil System.TimeoutException und CapSkip.TimeoutException beide im Gültigkeitsbereich liegen. Schreiben Sie den vollständigen CapSkip-Namen, wie oben. RecaptchaAsync gibt nach dem recaptchaTimeout des SDK auf, standardmäßig 300 Sekunden, das Sie im Konstruktor von CapSkipClient festlegen. Ein langsamer Lösevorgang scheitert meist schon früher innerhalb von CapSkip selbst und kommt dann als ApiException an.

Häufige Fehler beim Lösen von reCAPTCHA in WebView2

Was Sie sehenUrsacheBeheben
InvalidOperationException: CoreWebView2 can only be accessed from the UI threadDer Code nach dem Lösevorgang lief auf einem Thread-Pool-Thread weiter, durch ConfigureAwait(false) oder Task.RunVerwenden Sie im Event-Handler ein einfaches await
Der sitekey steht in Anführungszeichen, und jeder Lösevorgang scheitertDas rohe JSON von ExecuteScriptAsync ging an CapSkipDeserialisieren Sie das Ergebnis mit System.Text.Json
Der sitekey ist null, obwohl die Seite ein sichtbares Widget hatDie Seite rendert das Widget selbst oder fügt es spät hinzu, oder es sitzt in einem iframeLesen Sie das Anchor-iframe aus, pollen Sie nach dem Schlüssel oder führen Sie das Skript im Frame aus
Access is denied direkt beim Start der AppWebView2 wollte seinen Datenordner neben einer exe anlegen, an deren Speicherort der Benutzer nicht schreiben darfErstellen Sie die Umgebung mit einem Ordner unter den lokalen App-Daten
ArgumentException: bereits mit einer anderen Umgebung initialisiertSource wurde gesetzt, bevor Ihre Umgebung erstellt warInitialisieren Sie zuerst mit Ihrer Umgebung und navigieren Sie dann
Die Seite lädt nach dem Absenden erneut das Login-FormularNur ein Antwortfeld wurde gefüllt, der Callback lief nie, oder der Token war abgelaufenFüllen Sie jedes Feld, rufen Sie den Callback auf und setzen Sie den Token direkt nach dem Lösevorgang ein
Die App versucht, die Seite zu lösen, auf der sie nach dem Login landetNavigationCompleted feuert bei jeder NavigationEntfernen Sie den Handler, sobald ein sitekey gefunden ist
NetworkException: Der Zielcomputer hat die Verbindung aktiv verweigertCapSkip läuft nicht, oder Host oder Port sind falschStarten Sie CapSkip und gleichen Sie die Adresse mit seinen Verbindungseinstellungen ab
Ein Dialog mit einer NullReferenceException nach dem Schließen eines FenstersEin Lösevorgang wurde erst fertig, nachdem sein Formular geschlossen warÜbergeben Sie einen CancellationToken und brechen Sie ihn ab, wenn das Formular geschlossen wird

FAQ

Funktioniert das auch für reCAPTCHA v3 und Enterprise?

Ja, mit Optionen statt mit einer anderen Methode. Eine Enterprise-Seite lädt enterprise.js statt api.js. Fügen Sie den Optionen von RecaptchaAsync enterprise mit dem Wert 1 hinzu, und der Rest dieses Leitfadens bleibt gleich. Eine v3-Seite ist anders: Ihr sitekey steht im Parameter render von api.js, und die Seite sendet den Token aus ihrem eigenen Skript, nicht aus einer textarea. Übergeben Sie version mit dem Wert v3 sowie die action der Seite, und lassen Sie invisible weg, denn das lehnt das SDK für v3 ab. Der Leitfaden zum Lösen von reCAPTCHA v2 in C# behandelt die Optionen ausführlicher.

Wie unterscheide ich einen Skriptfehler von einem echten null?

Verwenden Sie ExecuteScriptWithResultAsync, das im aktuellen WebView2-Paket enthalten ist. Es meldet, ob das Skript erfolgreich war, und liefert Ihnen die Meldung der Exception, wenn nicht, während ExecuteScriptAsync in beiden Fällen null zurückgibt. Behalten Sie ExecuteScriptAsync für die einfachen Lesezugriffe oben, bei denen ein fehlendes Widget und ein kaputter Selektor dasselbe bedeuten: Es gibt nichts zu lösen.

Ist das dasselbe wie das Lösen von Captchas in einer mobilen WebView?

Nein. WebView2 ist die Edge-Engine in einer Windows-Desktop-App, und Sie steuern sie direkt aus C#. Eine WebView in einer Android- oder iOS-App wird von außen gesteuert, meist über Appium, das eigene Session-Timeouts hat. Diesen Fall behandelt der Leitfaden zum Lösen von Captchas in Appium.

Kann die Desktop-App auf einem anderen Rechner laufen als CapSkip?

Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, und übergeben Sie diese Adresse als Host von CapSkipClient. Die App ruft es dann über dieselbe HTTP-API von jedem Rechner aus auf, der den Port erreichen kann. Fügen Sie in der Windows Firewall eine Blockierregel für die Adressen hinzu, die es nicht erreichen sollen. Der Solver läuft weiterhin auf Hardware, die Ihnen gehört, und kein Lösevorgang läuft über einen Drittanbieterdienst.

Die Kurzfassung

Um reCAPTCHA in WebView2 zu lösen, lesen Sie den sitekey nach NavigationCompleted aus und deserialisieren das JSON, das ExecuteScriptAsync zurückgibt. Warten Sie RecaptchaAsync mit await ohne ConfigureAwait(false) ab, damit der nächste CoreWebView2-Aufruf auf dem UI-Thread stattfindet. Füllen Sie jedes Antwortfeld und rufen Sie dann den Callback der Seite auf oder verwenden Sie form.requestSubmit(). Geben Sie WebView2 einen beschreibbaren Datenordner, brechen Sie den Lösevorgang ab, wenn das Fenster geschlossen wird, und schalten Sie CapSkip in den Server-Modus, wenn die App auf einen anderen Rechner umzieht.

Eine Desktop-App und ihr Solver auf demselben Windows-PC sind das einfachste Setup überhaupt. Genau dafür ist ein lokaler Captcha-Löser da: Der Token kommt von Ihrem eigenen Rechner, und kein Lösevorgang verlässt ihn.