Intégrations

Résolution des CAPTCHA dans React Native WebViews avec CaptchaAI

Les applications React Native qui chargent du contenu Web via react-native-webview rencontrent fréquemment des CAPTCHA : cases à cocher reCAPTCHA v2, widgets Cloudflare Turnstile ou pages de défi qui bloquent les soumissions de formulaires.CaptchaAIrésout ces défis par programmation afin que les flux de vos applications mobiles restent ininterrompus.

Ce guide vous montre comment détecter les CAPTCHA dans un React Native WebView, extraire les paramètres requis via l'injection JavaScript, les envoyer à l'API CaptchaAI et réinjecter le jeton résolu dans la page.

Scénario du monde réel

Vous créez une application React Native qui charge un formulaire Web tiers dans une WebView. Le formulaire comprend une case à cocher reCAPTCHA v2 qui doit être remplie avant la soumission. Votre objectif est de :

  1. Détecter le widget CAPTCHA lorsque le chargement de WebView est terminé
  2. Extraire la clé de site du DOM
  3. Résolvez-le via l'API de CaptchaAI à partir d'un service backend
  4. Réinjectez le jeton dans WebView et soumettez le formulaire

Environnement : React Native 0.72+, réagissez-native-webview 13+, backend Node.js, API CaptchaAI.

Présentation de l'architecture

Le flux se divise en trois couches :

Calque Responsabilité
Réagir à WebView natif Détecte CAPTCHA, extrait la clé du site, injecte le jeton résolu
API back-end (Node.js) Reçoit sitekey + pageurl, appelle CaptchaAI, renvoie le jeton
API CaptchaAI Résout le CAPTCHA et renvoie le jeton

Le WebView communique avec votre code React Native via window.ReactNativeWebView.postMessage(), et votre backend gère l'interaction CaptchaAI pour garder les clés API hors du client.

Étape 1 : Détecter le CAPTCHA et extraire la clé du site dans WebView

Utilisez l'accessoire injectedJavaScript pour rechercher les éléments CAPTCHA une fois la page chargée :

// CaptchaDetector.js — React Native Component
import React, { useRef, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';

const CAPTCHA_DETECTION_SCRIPT = `
  (function() {
    // Detect reCAPTCHA v2
    const recaptchaDiv = document.querySelector('.g-recaptcha');
    if (recaptchaDiv) {
      const sitekey = recaptchaDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'recaptcha_v2',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    // Detect Cloudflare Turnstile
    const turnstileDiv = document.querySelector('.cf-turnstile');
    if (turnstileDiv) {
      const sitekey = turnstileDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'turnstile',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    window.ReactNativeWebView.postMessage(JSON.stringify({
      type: 'no_captcha'
    }));
  })();
  true;
`;

export default function CaptchaWebView({ url }) {
  const webviewRef = useRef(null);
  const [solving, setSolving] = useState(false);

  const handleMessage = async (event) => {
    const data = JSON.parse(event.nativeEvent.data);

    if (data.type === 'captcha_detected') {
      setSolving(true);
      try {
        const token = await solveCaptchaViaBackend(
          data.captchaType,
          data.sitekey,
          data.pageurl
        );
        injectToken(data.captchaType, token);
      } catch (err) {
        console.error('CAPTCHA solve failed:', err.message);
      } finally {
        setSolving(false);
      }
    }
  };

  const solveCaptchaViaBackend = async (captchaType, sitekey, pageurl) => {
    const response = await fetch('https://your-backend.com/api/solve-captcha', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ captchaType, sitekey, pageurl }),
    });
    const result = await response.json();
    if (!result.token) throw new Error(result.error || 'No token returned');
    return result.token;
  };

  const injectToken = (captchaType, token) => {
    let script;
    if (captchaType === 'recaptcha_v2') {
      script = `
        document.getElementById('g-recaptcha-response').value = '${token}';
        if (typeof ___grecaptcha_cfg !== 'undefined') {
          Object.keys(___grecaptcha_cfg.clients).forEach(key => {
            const client = ___grecaptcha_cfg.clients[key];
            Object.keys(client).forEach(k => {
              const item = client[k];
              if (item && item.callback) {
                item.callback('${token}');
              }
            });
          });
        }
        true;
      `;
    } else if (captchaType === 'turnstile') {
      script = `
        const input = document.querySelector('[name="cf-turnstile-response"]');
        if (input) input.value = '${token}';
        const callback = document.querySelector('.cf-turnstile')
          ?.getAttribute('data-callback');
        if (callback && typeof window[callback] === 'function') {
          window[callback]('${token}');
        }
        true;
      `;
    }
    webviewRef.current?.injectJavaScript(script);
  };

  return (
    <View style={{ flex: 1 }}>
      {solving && <ActivityIndicator size="large" />}
      <WebView
        ref={webviewRef}
        source={{ uri: url }}
        injectedJavaScript={CAPTCHA_DETECTION_SCRIPT}
        onMessage={handleMessage}
        javaScriptEnabled={true}
      />
    </View>
  );
}

Étape 2 : Créer le solveur backend (Node.js)

Conservez votre clé API CaptchaAI côté serveur. Le backend reçoit la clé du site et l'URL de la page, les soumet à CaptchaAI, interroge le résultat et renvoie le jeton :

// server.js — Express backend
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY || 'YOUR_API_KEY';

app.post('/api/solve-captcha', async (req, res) => {
  const { captchaType, sitekey, pageurl } = req.body;

  try {
    // Step 1: Submit task to CaptchaAI
    const submitParams = {
      key: API_KEY,
      pageurl: pageurl,
      json: '1',
    };

    if (captchaType === 'recaptcha_v2') {
      submitParams.method = 'userrecaptcha';
      submitParams.googlekey = sitekey;
    } else if (captchaType === 'turnstile') {
      submitParams.method = 'turnstile';
      submitParams.sitekey = sitekey;
    }

    const submitResponse = await axios.get(
      'https://ocr.captchaai.com/in.php',
      { params: submitParams }
    );

    if (submitResponse.data.status !== 1) {
      return res.status(400).json({ error: submitResponse.data.request });
    }

    const taskId = submitResponse.data.request;

    // Step 2: Poll for result
    const token = await pollForResult(taskId);
    res.json({ token });
  } catch (error) {
    console.error('Solve error:', error.message);
    res.status(500).json({ error: 'Failed to solve CAPTCHA' });
  }
});

async function pollForResult(taskId, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const response = await axios.get('https://ocr.captchaai.com/res.php', {
      params: {
        key: API_KEY,
        action: 'get',
        id: taskId,
        json: '1',
      },
    });

    if (response.data.status === 1) {
      return response.data.request;
    }

    if (
      response.data.request !== 'CAPCHA_NOT_READY' &&
      response.data.status === 0
    ) {
      throw new Error(response.data.request);
    }
  }
  throw new Error('Polling timeout — CAPTCHA not solved in time');
}

app.listen(3000, () => console.log('Solver backend running on port 3000'));

Étape 3 : Gérer l'expiration du jeton dans WebView

Les jetons CAPTCHA expirent : les jetons reCAPTCHA v2 durent environ 120 secondes, les jetons Turnstile environ 300 secondes. Si l'utilisateur retarde la soumission du formulaire, résolvez-le avant de le soumettre :

// Add to CaptchaWebView component
const [tokenTimestamp, setTokenTimestamp] = useState(null);
const TOKEN_TTL_MS = 110000; // 110 seconds for reCAPTCHA v2

const handleFormSubmit = async (captchaType, sitekey, pageurl) => {
  const now = Date.now();
  if (!tokenTimestamp || now - tokenTimestamp > TOKEN_TTL_MS) {
    const freshToken = await solveCaptchaViaBackend(
      captchaType, sitekey, pageurl
    );
    injectToken(captchaType, freshToken);
    setTokenTimestamp(Date.now());
  }

  webviewRef.current?.injectJavaScript(`
    document.querySelector('form').submit();
    true;
  `);
};

Dépannage

Problème Parce que Corriger
postMessage non reçu WebView onMessage non défini ou erreur de script Vérifiez que onMessage est lié ; envelopper le script d'injection dans try/catch
ERROR_BAD_TOKEN_OR_PAGEURL Sitekey ne correspond pas à l'URL de la page Extraire la clé de site du src iframe réel, pas de la page parent
L'injection de jetons ne déclenche pas de rappel Rappel reCAPTCHA introuvable dans ___grecaptcha_cfg Itérer tous les objets clients et vérifier les propriétés imbriquées pour les fonctions de rappel
CAPCHA_NOT_READY indéfiniment Résolution lente ou paramètres invalides Augmenter le délai d'attente du sondage ; vérifier que la clé du site et l'URL de la page sont correctes
WebView affiche un CAPTCHA vide JavaScript désactivé ou contenu bloqué Définissez javaScriptEnabled={true} et assurez-vous qu'aucun blocage de politique de sécurité du contenu

FAQ

Puis-je appeler CaptchaAI directement depuis React Native sans backend ?

Techniquement oui, mais votre clé API serait exposée dans le binaire de l'application. Acheminez toujours les appels d’API via votre propre backend pour sécuriser les informations d’identification.

Cela fonctionne-t-il avec le flux de travail géré par Expo ?

Oui, react-native-webview est pris en charge dans Expo SDK 49+ avec expo-dev-client. L'injection JavaScript et la transmission de messages fonctionnent de manière identique.

Comment gérer les pages avec reCAPTCHA et Turnstile ?

Le script de détection vérifie les deux. Si une page comporte plusieurs CAPTCHA, étendez le script pour collecter toutes les clés de site et résolvez-les séquentiellement avant la soumission du formulaire.

Quel est le temps de résolution moyen des CAPTCHA mobiles ?

Les résolutions de reCAPTCHA v2 prennent généralement 10 à 20 secondes. Cloudflare Turnstile est plus rapide à 5-15 secondes. Planifiez votre UX pour afficher un indicateur de chargement pendant cette fenêtre.

L'agent utilisateur de WebView affecte-t-il les taux de résolution ?

React Native WebView utilise l'agent utilisateur mobile par défaut de l'appareil, qui correspond bien à la façon dont les utilisateurs réels naviguent. Cela produit généralement de bons taux de résolution sans modification.

Articles connexes

Prochaines étapes

Commencez à résoudre les CAPTCHA dans vos applications React Native –récupérez votre clé API CaptchaAIet intégrez le solveur backend dès aujourd'hui.

Guides associés :

Les commentaires sont désactivés pour cet article.