Os aplicativos React Native que carregam conteúdo da web por meio de react-native-webview frequentemente encontram CAPTCHAs – caixas de seleção reCAPTCHA v2, widgets Cloudflare Turnstile ou páginas de desafio que bloqueiam envios de formulários.CaptchaAIresolve esses desafios de forma programática para que os fluxos de seus aplicativos móveis permaneçam ininterruptos.
Este guia mostra como detectar CAPTCHAs dentro de um React Native WebView, extrair os parâmetros necessários por meio de injeção de JavaScript, enviá-los para a API CaptchaAI e injetar o token resolvido de volta na página.
Cenário do mundo real
Você está construindo um aplicativo React Native que carrega um formulário web de terceiros dentro de um WebView. O formulário inclui uma caixa de seleção reCAPTCHA v2 que deve ser preenchida antes do envio. Seu objetivo é:
- Detecte o widget CAPTCHA quando o WebView terminar de carregar
- Extraia a chave do site do DOM
- Resolva-o através da API do CaptchaAI a partir de um serviço backend
- Injete o token de volta no WebView e envie o formulário
Ambiente: React Native 0.72+, react-native-webview 13+, backend Node.js, API CaptchaAI.
Visão geral da arquitetura
O fluxo se divide em três camadas:
| Camada | Responsabilidade |
|---|---|
| Reagir WebView nativo | Detecta CAPTCHA, extrai sitekey, injeta token resolvido |
| API de back-end (Node.js) | Recebe sitekey + pageurl, chama CaptchaAI, retorna token |
| API CaptchaAI | Resolve o CAPTCHA e retorna o token |
O WebView se comunica com seu código React Native via window.ReactNativeWebView.postMessage(), e seu backend lida com a interação CaptchaAI para manter as chaves de API fora do cliente.
Etapa 1: detectar CAPTCHA e extrair Sitekey no WebView
Use a propriedade injectedJavaScript para procurar elementos CAPTCHA assim que a página carregar:
// 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>
);
}
Etapa 2: construir o solucionador de back-end (Node.js)
Mantenha sua chave API CaptchaAI no lado do servidor. O backend recebe a chave do site e o URL da página, envia para CaptchaAI, pesquisa o resultado e retorna o token:
// 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'));
Etapa 3: lidar com a expiração do token no WebView
Os tokens CAPTCHA expiram - os tokens reCAPTCHA v2 duram aproximadamente 120 segundos, os tokens Turnstile aproximadamente 300 segundos. Se o usuário atrasar o envio do formulário, resolva antes de enviar:
// 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;
`);
};
Solução de problemas
| Problema | Causa | Correção |
|---|---|---|
postMessage não recebido |
WebView onMessage não definido ou erro de script |
Verifique se onMessage está vinculado; envolver script de injeção em try/catch |
ERROR_BAD_TOKEN_OR_PAGEURL |
Sitekey não corresponde ao URL da página | Extraia o sitekey do iframe src real, não da página pai |
| A envio controlado ao endpoint QA não aciona retorno de chamada | Retorno de chamada reCAPTCHA não encontrado em ___grecaptcha_cfg |
Itere todos os objetos do cliente e verifique as propriedades aninhadas para funções de retorno de chamada |
CAPCHA_NOT_READY indefinidamente |
Resolução lenta ou parâmetros inválidos | Aumentar o tempo limite da votação; verifique se a chave do site e o pageurl estão corretos |
| WebView mostra CAPTCHA em branco | JavaScript desativado ou conteúdo bloqueado | Defina javaScriptEnabled={true} e garanta que não haja bloqueios de política de segurança de conteúdo |
Perguntas frequentes
Posso ligar para CaptchaAI diretamente do React Native sem back-end?
Tecnicamente sim, mas sua chave de API seria exposta no binário do aplicativo. Sempre encaminhe chamadas de API por meio de seu próprio back-end para manter as credenciais seguras.
Isso funciona com o fluxo de trabalho gerenciado pela Expo?
Sim, react-native-webview é compatível com Expo SDK 49+ com expo-dev-client. A injeção de JavaScript e a passagem de mensagens funcionam de forma idêntica.
Como lidar com páginas com reCAPTCHA e Turnstile?
O script de detecção verifica ambos. Se uma página tiver vários CAPTCHAs, estenda o script para coletar todas as chaves do site e resolvê-las sequencialmente antes do envio do formulário.
Qual é o tempo médio de resolução para CAPTCHAs móveis?
As soluções de reCAPTCHA v2 normalmente levam de 10 a 20 segundos. Cloudflare Turnstile é com menor latência em 5 a 15 segundos. Planeje sua experiência do usuário para mostrar um indicador de carregamento durante esta janela.
O User-Agent do WebView afeta as taxas de resolução?
O React Native WebView usa o User-Agent móvel padrão do dispositivo, que se alinha bem com a forma como os usuários reais navegam. Isso geralmente produz boas taxas de resolução sem modificação.
Artigos relacionados
- Como resolver o retorno de chamada do Recaptcha V2 usando API
- Comparação de Geetest vs Cloudflare Turnstile
- Torniquete Recaptcha V2 no mesmo local
Próximas etapas
Comece a resolver CAPTCHAs em seus aplicativos React Native —obtenha sua chave API CaptchaAIe integre o solucionador de back-end hoje mesmo.
Guias relacionados: