如果你的 React Native App 通过 react-native-webview 加载第三方网页,迟早会撞上验证码:reCAPTCHA v2 复选框、Cloudflare Turnstile 小部件,或者直接卡住提交的质询页面。WebView 内部的 DOM 对原生代码不可见,处理起来比桌面网页更棘手。
解决思路并不复杂:用 injectedJavaScript 往 WebView 注入检测脚本,把 sitekey 和页面 URL 通过 postMessage 传回 React Native,再由后端调用 CaptchaAI API 完成识别,最后把 token 注入回页面并提交表单。本文按这条链路搭建方案,并给出常见故障的排查方法。
适用场景: App 内嵌 WebView 加载第三方表单、且能自行部署后端服务的场景;原生页面验证码请参考其他 CaptchaAI 集成指南。
整体架构:三层协作
整套流程分三层,各司其职:
| 层 | 职责 |
|---|---|
| React Native WebView | 检测验证码、提取 sitekey、注入识别结果 |
| 后端 API(Node.js) | 接收 sitekey + pageurl,调用 CaptchaAI,返回 token |
| CaptchaAI API | 识别验证码并返回 token |
WebView 通过 window.ReactNativeWebView.postMessage() 与 React Native 代码通信;识别请求全部走你自己的后端,API Key 不会出现在客户端代码里。
典型场景:WebView 里卡住的 reCAPTCHA 表单
落到具体场景:假设你在做一个 React Native App,在 WebView 中加载第三方网页表单,表单带一个必须勾选的 reCAPTCHA v2 复选框。具体步骤是:
- WebView 加载完成后检测页面里的验证码控件
- 从 DOM 中提取 sitekey
- 通过后端服务调用 CaptchaAI API 完成识别
- 把 token 注入回 WebView 并提交表单
环境要求:
- React Native 0.72+
- react-native-webview 13+
- Node.js 后端
- CaptchaAI API
第 1 步:在 WebView 中检测验证码并提取 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>
);
}
脚本依次查找 .g-recaptcha 和 .cf-turnstile,都没有就回传 no_captcha;CaptchaWebView 收到 captcha_detected 后调用后端识别,再把结果注入回页面。
小提示: 建议给脚本包一层 try/catch,避免选择器失效时检测逻辑整体崩溃。
第 2 步:用 Node.js 搭建后端识别服务
CaptchaAI 的 API Key 只能留在服务端,绝不能打进 App 安装包。后端接收 sitekey 和页面 URL,提交给 CaptchaAI,轮询结果,再把 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'));
参数名不要记混:
reCAPTCHA v2→method: userrecaptcha+googlekeyTurnstile→method: turnstile+sitekey
pollForResult 每 5 秒轮询一次 res.php,status !== 1 且不是 CAPCHA_NOT_READY 时说明识别失败,需抛出具体错误信息。
第 3 步:处理 WebView 中的 token 过期
token 是有时效的:reCAPTCHA v2 的 token 大约 120 秒后失效,Turnstile 的 token 大约能撑 300 秒。如果用户在填表单时拖延了很久才点提交,提交前最好重新判断一次 token 是否还新鲜:
// 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;
`);
};
TOKEN_TTL_MS 留了 10 秒缓冲,避免拿着刚好过期的 token 去提交。reCAPTCHA v2 识别一般耗时 10–20 秒,Turnstile 更快,通常 5–15 秒,用加载动画覆盖这段等待即可。
排查清单:WebView 验证码常见故障
| 问题 | 原因 | 处理方式 |
|---|---|---|
收不到 postMessage |
onMessage 没绑定,或注入脚本报错 |
检查 onMessage 是否已设置;脚本包一层 try/catch |
ERROR_BAD_TOKEN_OR_PAGEURL |
sitekey 和 pageurl 对不上 | 从实际弹出验证码的 iframe src 取 sitekey,而不是父页面 |
| token 注入后回调没触发 | ___grecaptcha_cfg 里没找到对应回调 |
遍历所有 client 对象,检查嵌套属性里的回调函数 |
CAPCHA_NOT_READY 一直返回 |
识别耗时较长,或参数有误 | 延长轮询超时;核对 sitekey 和 pageurl 是否正确 |
| WebView 里验证码空白 | JavaScript 被禁用,或内容被安全策略拦截 | 设置 javaScriptEnabled={true},确认无内容安全策略挡住脚本 |
WebView 默认使用设备自身的移动端 User-Agent,通常无需额外处理。
常见问题
为什么必须走后端调用 CaptchaAI,不能直接在 App 里请求?
技术上可以,但 API Key 会被打包进安装包,反编译即可获取;识别请求放在后端才安全。
WebView 检测不到验证码元素,一般是什么原因?
多是控件还没渲染完脚本就先跑了,或验证码在 iframe 里而选择器扫的是外层文档。可在 onLoadEnd 后延迟检测,或加一层轮询。
国内网络环境下 reCAPTCHA 加载慢,会影响识别成功率吗?
reCAPTCHA 脚本托管在 Google,国内网络访问不稳定,容易导致 sitekey 提取超时,但这属于网络连通性问题,不影响 CaptchaAI 这一侧的识别成功率;Cloudflare Turnstile 走自己的 CDN,通常不受影响。
Expo 托管工作流下能用这套方案吗?
可以。Expo SDK 49+ 配合 expo-dev-client 即可用 react-native-webview,注入和消息传递方式和裸项目一样。
下一步
现在就开始在 React Native App 里识别验证码——获取你的 CaptchaAI API 密钥,把后端识别服务接入你的项目。
相关文章
- 如何用 API 解决 reCAPTCHA v2 回调
- GeeTest 与 Cloudflare Turnstile 对比
- reCAPTCHA v2 与 Turnstile 同站点处理
相关指南: