集成指南

使用 CaptchaAI 解决 React Native WebView 中的验证码问题

通过 react-native-webview 加载 Web 内容的 React Native 应用程序经常会遇到验证码 - reCAPTCHA v2 复选框、Cloudflare Turnstile 小部件或阻止表单提交的质询页面。CaptchaAI以编程方式解决这些挑战,使您的移动应用程序流程保持不间断。

本指南向您展示如何在 React Native WebView 中检测验证码,通过 JavaScript 注入提取所需的参数,将它们发送到 CaptchaAI API,并将解析后的token 提交回页面。

真实场景

您正在构建一个 React Native 应用程序,该应用程序在 WebView 中加载第三方 Web 表单。该表格包含一个 reCAPTCHA v2 复选框,必须在提交前填写。您的目标是:

  1. WebView 加载完成时检测 CAPTCHA 小部件
  2. 从 DOM 中提取 sitekey
  3. 通过后端服务的 CaptchaAI 的 API 解决它
  4. 将token 提交回 WebView 并提交表单

环境: React Native 0.72+、react-native-webview 13+、Node.js 后端、CaptchaAI API。

架构概述

该流程分为三层:

责任
反应本机 WebView 检测验证码、提取站点密钥、注入已解析的令牌
后端 API (Node.js) 接收sitekey + pageurl,调用CaptchaAI,返回token
CaptchaAI API 解决验证码并返回令牌

WebView 通过 window.ReactNativeWebView.postMessage() 与您的 React Native 代码进行通信,并且您的后端处理 CaptchaAI 交互以使 API 密钥远离客户端。

第1步:在WebView中检测CAPTCHA并提取Sitekey

页面加载后,使用 injectedJavaScript 属性扫描验证码元素:

// 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>
  );
}

第 2 步:构建后端求解器 (Node.js)

将您的 CaptchaAI API 密钥保留在服务器端。后端接收sitekey和页面URL,提交到CaptchaAI,轮询结果,并返回令牌:

// 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'));

步骤 3:在 WebView 中处理令牌过期

CAPTCHA 令牌过期 - reCAPTCHA v2 令牌持续约 120 秒,Turnstile 令牌约 300 秒。如果用户延迟表单提交,请在提交前重新解决:

// 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;
  `);
};

故障排除

问题 原因 处理方式
postMessage 未收到 WebView onMessage 未设置或脚本错误 检查onMessage是否已绑定;将注入脚本包装在 try/catch 中
ERROR_BAD_TOKEN_OR_PAGEURL Sitekey 与页面 URL 不匹配 从实际的 iframe src 中提取 sitekey,而不是父页面
token 提交不会触发回调 ___grecaptcha_cfg 中未找到 reCAPTCHA 回调 迭代所有客户端对象并检查回调函数的嵌套属性
CAPCHA_NOT_READY 无限期 求解速度慢或参数无效 增加轮询超时时间;验证 sitekey 和 pageurl 是否正确
WebView 显示空白验证码 JavaScript 被禁用或内容被阻止 设置 javaScriptEnabled={true} 并确保没有内容安全策略阻止

常问问题

我可以在没有后端的情况下直接从 React Native 调用 CaptchaAI 吗?

从技术上讲是可以的,但是您的 API 密钥将在应用程序二进制文件中公开。始终通过您自己的后端路由 API 调用,以确保凭据安全。

这适用于 Expo 管理的工作流程吗?

是的,Expo SDK 49+ 中的 expo-dev-client 支持 react-native-webview。 JavaScript 注入和消息传递的工作方式相同。

如何同时使用 reCAPTCHA 和 Turnstile 处理页面?

检测脚本会检查两者。如果页面有多个验证码,请扩展脚本以收集所有站点密钥并在表单提交之前按顺序解决它们。

移动验证码的平均解决时间是多少?

reCAPTCHA v2 求解通常需要 10-20 秒。 Cloudflare Turnstile 速度更快,为 5-15 秒。规划您的用户体验以在此窗口中显示加载指示器。

WebView 的用户代理会影响解决率吗?

React Native WebView 使用设备的默认移动用户代理,这与真实用户的浏览方式非常一致。这通常会在不进行修改的情况下产生良好的解决率。

相关文章

下一步

开始在 React Native 应用程序中解决验证码问题 –”获取您的 CaptchaAI API 密钥今天集成后端求解器。

相关指南:

该文章已禁用评论。