Các ứng dụng React Native tải nội dung web thông qua react-native-webview thường gặp phải CAPTCHA - hộp kiểm reCAPTCHA v2, tiện ích Cloudflare Turnstile hoặc các trang thử thách chặn việc gửi biểu mẫu.CaptchaAIgiải quyết những thách thức này theo chương trình để luồng ứng dụng dành cho thiết bị di động của bạn không bị gián đoạn.
Hướng dẫn này chỉ cho bạn cách phát hiện CAPTCHA bên trong React Native WebView, trích xuất các tham số cần thiết thông qua tính năng chèn JavaScript, gửi chúng đến API CaptchaAI và đưa mã thông báo đã giải quyết trở lại trang.
Kịch bản thế giới thực
Bạn đang xây dựng một ứng dụng React Native tải biểu mẫu web của bên thứ ba bên trong WebView. Biểu mẫu bao gồm hộp kiểm reCAPTCHA v2 phải được hoàn thành trước khi gửi. Mục tiêu của bạn là:
- Phát hiện tiện ích CAPTCHA khi WebView tải xong
- Trích xuất khóa trang web từ DOM
- Giải quyết vấn đề này thông qua API của CaptchaAI từ dịch vụ phụ trợ
- Đưa mã thông báo trở lại WebView và gửi biểu mẫu
Môi trường: React Native 0.72+, Reac-native-webview 13+, chương trình phụ trợ Node.js, API CaptchaAI.
Tổng quan về kiến trúc
Dòng chảy chia thành ba lớp:
| Lớp | Trách nhiệm |
|---|---|
| Phản ứng WebView gốc | Phát hiện CAPTCHA, trích xuất khóa trang web, chèn mã thông báo đã giải quyết |
| API phụ trợ (Node.js) | Nhận sitekey + pageurl, gọi CaptchaAI, trả lại mã thông báo |
| API CaptchaAI | Giải CAPTCHA và trả về mã thông báo |
WebView giao tiếp với mã React Native của bạn thông qua window.ReactNativeWebView.postMessage() và phần phụ trợ của bạn xử lý tương tác CaptchaAI để loại bỏ các khóa API khỏi máy khách.
Bước 1: Phát hiện CAPTCHA và trích xuất Sitekey trong WebView
Sử dụng giá trị injectedJavaScript để quét các phần tử CAPTCHA sau khi tải trang:
// 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>
);
}
Bước 2: Xây dựng Bộ giải phụ trợ (Node.js)
Giữ khóa API CaptchaAI của bạn ở phía máy chủ. Phần phụ trợ nhận khóa trang và URL trang, gửi tới CaptchaAI, thăm dò kết quả và trả về mã thông báo:
// 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'));
Bước 3: Xử lý hết hạn mã thông báo trong WebView
Mã thông báo CAPTCHA hết hạn - mã thông báo reCAPTCHA v2 tồn tại ~120 giây, mã thông báo Turnstile ~300 giây. Nếu người dùng trì hoãn việc gửi biểu mẫu, hãy giải quyết lại trước khi gửi:
// 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;
`);
};
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
postMessage không nhận được |
WebView onMessage chưa được đặt hoặc lỗi tập lệnh |
Kiểm tra onMessage có bị ràng buộc không; gói tập lệnh tiêm trong try/catch |
ERROR_BAD_TOKEN_OR_PAGEURL |
Khóa trang web không khớp với URL trang | Trích xuất khóa trang web từ iframe src thực tế, không phải trang gốc |
| Việc tiêm mã thông báo không kích hoạt lệnh gọi lại | Không tìm thấy lệnh gọi lại reCAPTCHA trong ___grecaptcha_cfg |
Lặp lại tất cả các đối tượng máy khách và kiểm tra các thuộc tính lồng nhau để tìm các hàm gọi lại |
CAPCHA_NOT_READY vô thời hạn |
Giải quyết chậm hoặc tham số không hợp lệ | Tăng thời gian bỏ phiếu; xác minh sitekey và pageurl là chính xác |
| WebView hiển thị CAPTCHA trống | JavaScript bị vô hiệu hóa hoặc nội dung bị chặn | Đặt javaScriptEnabled={true} và đảm bảo không chặn chính sách bảo mật nội dung |
Câu hỏi thường gặp
Tôi có thể gọi CaptchaAI trực tiếp từ React Native mà không cần phụ trợ không?
Về mặt kỹ thuật thì có, nhưng khóa API của bạn sẽ hiển thị trong tệp nhị phân của ứng dụng. Luôn định tuyến các lệnh gọi API thông qua chương trình phụ trợ của riêng bạn để bảo mật thông tin xác thực.
Tính năng này có hoạt động với quy trình làm việc do Expo quản lý không?
Có, react-native-webview được hỗ trợ trong Expo SDK 49+ với expo-dev-client. Việc chèn JavaScript và truyền tin nhắn hoạt động giống hệt nhau.
Làm cách nào để xử lý các trang có cả reCAPTCHA và Turnstile?
Tập lệnh phát hiện sẽ kiểm tra cả hai. Nếu một trang có nhiều CAPTCHA, hãy mở rộng tập lệnh để thu thập tất cả các khóa trang web và giải quyết chúng một cách tuần tự trước khi gửi biểu mẫu.
Thời gian giải quyết trung bình cho CAPTCHA trên thiết bị di động là bao lâu?
Việc giải reCAPTCHA v2 thường mất 10-20 giây. Cloudflare Turnstile nhanh hơn ở mức 5-15 giây. Lập kế hoạch cho UX của bạn hiển thị chỉ báo tải trong cửa sổ này.
Tác nhân người dùng của WebView có ảnh hưởng đến tỷ lệ giải quyết không?
React Native WebView sử dụng Tác nhân người dùng di động mặc định của thiết bị, phù hợp với cách người dùng thực duyệt qua. Điều này thường tạo ra tỷ lệ giải quyết tốt mà không cần sửa đổi.
bài viết liên quan
- Cách giải quyết cuộc gọi lại Recaptcha V2 bằng Api
- So sánh Geetest và Cloudflare Turnstile
- Xử lý cửa quay Recaptcha V2 trên cùng một trang web
Các bước tiếp theo
Bắt đầu giải CAPTCHA trong ứng dụng React Native của bạn —lấy khóa API CaptchaAI của bạnvà tích hợp bộ giải phụ trợ ngay hôm nay.
Hướng dẫn liên quan: