Các ứng dụng rung tải nội dung web thông qua webview_flutter hoặc flutter_inappwebview thường xuyên gặp phải CAPTCHA chặn luồng người dùng.CaptchaAIgiải quyết những thách thức này thông qua API, cho phép ứng dụng Flutter của bạn phát hiện, giải quyết và tự động đưa mã thông báo CAPTCHA vào trong WebView.
Hướng dẫn này bao gồm việc phát hiện CAPTCHA thông qua các kênh JavaScript, tích hợp bộ giải phụ trợ và chèn mã thông báo cho reCAPTCHA v2 và Cloudflare Turnstile.
Kịch bản thế giới thực
Ứng dụng Flutter của bạn nhúng cổng thanh toán vào WebView. Cổng đưa ra thử thách reCAPTCHA v2 trước khi xử lý. Bạn cần phải:
- Phát hiện tiện ích CAPTCHA sau khi tải WebView
- Trích xuất khóa trang web thông qua kênh JavaScript
- Giải quyết nó thông qua CaptchaAI từ dịch vụ phụ trợ
- Chèn mã thông báo và kích hoạt lệnh gọi lại
Môi trường: Flutter 3.16+, webview_flutter 4.x, Dart backend hoặc API Node.js, API CaptchaAI.
Bước 1: Thiết lập WebView với các kênh JavaScript
Sử dụng webview_flutter với kênh JavaScript để nhận thông báo phát hiện CAPTCHA từ trang được tải:
// captcha_webview.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
class CaptchaWebView extends StatefulWidget {
final String url;
const CaptchaWebView({super.key, required this.url});
@override
State<CaptchaWebView> createState() => _CaptchaWebViewState();
}
class _CaptchaWebViewState extends State<CaptchaWebView> {
late final WebViewController _controller;
bool _solving = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'CaptchaChannel',
onMessageReceived: _onCaptchaMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) => _detectCaptcha(),
),
)
..loadRequest(Uri.parse(widget.url));
}
Future<void> _detectCaptcha() async {
await _controller.runJavaScript('''
(function() {
var recaptcha = document.querySelector('.g-recaptcha');
if (recaptcha) {
CaptchaChannel.postMessage(JSON.stringify({
type: 'captcha_detected',
captchaType: 'recaptcha_v2',
sitekey: recaptcha.getAttribute('data-sitekey'),
pageurl: window.location.href
}));
return;
}
var turnstile = document.querySelector('.cf-turnstile');
if (turnstile) {
CaptchaChannel.postMessage(JSON.stringify({
type: 'captcha_detected',
captchaType: 'turnstile',
sitekey: turnstile.getAttribute('data-sitekey'),
pageurl: window.location.href
}));
return;
}
CaptchaChannel.postMessage(JSON.stringify({type: 'no_captcha'}));
})();
''');
}
Future<void> _onCaptchaMessage(JavaScriptMessage message) async {
final data = jsonDecode(message.message);
if (data['type'] != 'captcha_detected') return;
setState(() => _solving = true);
try {
final token = await _solveCaptcha(
data['captchaType'],
data['sitekey'],
data['pageurl'],
);
await _injectToken(data['captchaType'], token);
} catch (e) {
debugPrint('CAPTCHA solve failed: $e');
} finally {
setState(() => _solving = false);
}
}
Future<String> _solveCaptcha(
String captchaType, String sitekey, String pageurl,
) async {
final response = await http.post(
Uri.parse('https://your-backend.com/api/solve-captcha'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'captchaType': captchaType,
'sitekey': sitekey,
'pageurl': pageurl,
}),
);
final result = jsonDecode(response.body);
if (result['token'] == null) {
throw Exception(result['error'] ?? 'No token returned');
}
return result['token'];
}
Future<void> _injectToken(String captchaType, String token) async {
if (captchaType == 'recaptcha_v2') {
await _controller.runJavaScript('''
document.getElementById('g-recaptcha-response').value = '$token';
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.keys(___grecaptcha_cfg.clients).forEach(function(key) {
var client = ___grecaptcha_cfg.clients[key];
Object.keys(client).forEach(function(k) {
if (client[k] && client[k].callback) {
client[k].callback('$token');
}
});
});
}
''');
} else if (captchaType == 'turnstile') {
await _controller.runJavaScript('''
var input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = '$token';
var cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb]('$token');
''');
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(controller: _controller),
if (_solving)
const Center(child: CircularProgressIndicator()),
],
);
}
}
Bước 2: Bộ giải phụ trợ (Python)
Phần phụ trợ giữ an toàn cho khóa API của bạn và xử lý giao tiếp CaptchaAI:
# solver_api.py — Flask backend
import os
import time
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = os.environ.get("CAPTCHAAI_API_KEY", "YOUR_API_KEY")
@app.route("/api/solve-captcha", methods=["POST"])
def solve_captcha():
data = request.json
captcha_type = data.get("captchaType")
sitekey = data.get("sitekey")
pageurl = data.get("pageurl")
# Submit task
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if captcha_type == "recaptcha_v2":
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
elif captcha_type == "turnstile":
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
return jsonify({"error": f"Unsupported type: {captcha_type}"}), 400
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
result = resp.json()
if result.get("status") != 1:
return jsonify({"error": result.get("request", "Submit failed")}), 400
task_id = result["request"]
# Poll for result
for _ in range(30):
time.sleep(5)
poll_resp = requests.get(
"https://ocr.captchaai.com/res.php",
params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
},
)
poll_result = poll_resp.json()
if poll_result.get("status") == 1:
return jsonify({"token": poll_result["request"]})
if poll_result.get("request") != "CAPCHA_NOT_READY":
return jsonify({"error": poll_result["request"]}), 400
return jsonify({"error": "Timeout — CAPTCHA not solved"}), 408
if __name__ == "__main__":
app.run(port=3000)
Bước 3: Sử dụng Flutter_inappwebview (Thay thế)
Nếu bạn cần kiểm soát nhiều hơn – chặn các yêu cầu mạng, xử lý cookie hoặc quản lý nhiều WebView – hãy sử dụng flutter_inappwebview:
// Using flutter_inappwebview for advanced CAPTCHA handling
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36',
),
onLoadStop: (controller, url) async {
// Evaluate JavaScript and get result directly
final result = await controller.evaluateJavascript(source: '''
(function() {
var el = document.querySelector('.g-recaptcha');
if (el) return JSON.stringify({
sitekey: el.getAttribute('data-sitekey'),
pageurl: window.location.href
});
return null;
})();
''');
if (result != null) {
final data = jsonDecode(result);
// Solve and inject token
final token = await _solveCaptcha(
'recaptcha_v2', data['sitekey'], data['pageurl'],
);
await controller.evaluateJavascript(source: '''
document.getElementById('g-recaptcha-response').value = '$token';
''');
}
},
)
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Kênh JavaScript không nhận được tin nhắn | Tên kênh không khớp | Đảm bảo CaptchaChannel khớp chính xác giữa Dart và JS |
ERROR_BAD_TOKEN_OR_PAGEURL từ CaptchaAI |
Khóa trang web từ iframe sai | Trích xuất khóa trang web từ iframe CAPTCHA, không phải khung chính |
| Việc tiêm mã thông báo không có tác dụng | Vùng văn bản bị ẩn hoặc gọi lại không được kích hoạt | Đặt giá trị g-recaptcha-response VÀ kích hoạt chức năng gọi lại |
CAPCHA_NOT_READY tiếp tục bỏ phiếu |
Giải quyết chậm hoặc tham số không hợp lệ | Xác minh sitekey và pageurl; tăng số lần bỏ phiếu tối đa |
| WebView gặp sự cố trên trang CAPTCHA | Vấn đề về bộ nhớ với các trang nặng | Sử dụng flutter_inappwebview với useHybridComposition: true trên Android |
Câu hỏi thường gặp
Tôi nên sử dụng webview_flutter hay Flutter_inappwebview?
webview_flutter bao gồm hầu hết các trường hợp. Sử dụng flutter_inappwebview khi bạn cần quản lý cookie, yêu cầu chặn hoặc đánh giá JavaScript trực tiếp bằng các giá trị trả về.
Tôi có thể giải CAPTCHA mà không cần máy chủ phụ trợ không?
Bạn có thể gọi CaptchaAI trực tiếp từ Dart, nhưng điều này sẽ làm lộ khóa API của bạn trong tệp nhị phân của ứng dụng. Luôn định tuyến thông qua phần phụ trợ cho các ứng dụng sản xuất.
Làm cách nào để xử lý việc hết hạn mã thông báo CAPTCHA trong Flutter?
Mã thông báo reCAPTCHA v2 sẽ hết hạn sau ~120 giây. Theo dõi thời điểm nhận được mã thông báo và giải quyết lại nếu người dùng chưa gửi biểu mẫu trong cửa sổ đó.
Tính năng này có hoạt động trên cả Android và iOS không?
Vâng. Cả webview_flutter và flutter_inappwebview đều hỗ trợ Android và iOS. Việc chèn JavaScript và giao tiếp kênh hoạt động giống hệt nhau trên cả hai nền tảng.
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 Flutter của bạn —lấy khóa API CaptchaAI của bạnvà kết nối bộ giải phụ trợ của bạn.
Hướng dẫn liên quan:
- Phản ứng giải quyết CAPTCHA WebView gốc
- Xử lý CAPTCHA trong Tự động hóa ứng dụng di động với Appium
- Trích xuất khóa trang web Cloudflare Turnstile