Aplikasi Flutter yang memuat konten web melalui webview_flutter atau flutter_inappwebview sering menemukan CAPTCHA yang memblokir alur pengguna. CaptchaAI menyelesaikan challenge ini via API, memungkinkan aplikasi Flutter Anda mendeteksi, solve, dan menyuntikkan token CAPTCHA secara otomatis ke dalam WebViews.
Panduan ini mencakup deteksi CAPTCHA via JavaScript channel, integrasi backend solver, dan pengaturan token untuk reCAPTCHA v2 dan Cloudflare Turnstile.
Skenario Dunia Nyata
Aplikasi Flutter Anda menyematkan payment gateway di WebView. Gateway menampilkan challenge reCAPTCHA v2 sebelum pemrosesan. Anda perlu:
- Deteksi widget CAPTCHA setelah WebView dimuat
- Ekstrak sitekey via JavaScript channel
- Solve via CaptchaAI dari layanan backend
- Inject token dan trigger callback
Environment: Flutter 3.16+, webview_flutter 4.x, Dart backend atau Node.js API, CaptchaAI API.
Langkah 1: Setup WebView dengan JavaScript Channel
Gunakan webview_flutter dengan JavaScript channel untuk menerima pesan deteksi CAPTCHA dari halaman yang dimuat:
// 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()),
],
);
}
}
Langkah 2: Backend Solver (Python)
Backend menjaga API key Anda tetap aman dan menangani komunikasi dengan 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)
Langkah 3: Menggunakan flutter_inappwebview (Alternatif)
Jika Anda memerlukan kontrol lebih besar — intercept request jaringan, mengelola cookie, atau beberapa WebView — gunakan 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';
''');
}
},
)
Pemecahan Masalah
| Masalah | Penyebab | Perbaikan |
|---|---|---|
| JavaScript channel tidak menerima pesan | Nama channel tidak cocok | Pastikan CaptchaChannel sama persis antara Dart dan JS |
ERROR_BAD_TOKEN_OR_PAGEURL dari CaptchaAI |
Sitekey dari iframe yang salah | Ekstrak sitekey dari iframe CAPTCHA, bukan parent frame |
| pengaturan token tidak berpengaruh | Textarea tersembunyi atau callback tidak terpicu | Set nilai g-recaptcha-response DAN aktifkan fungsi callback |
CAPCHA_NOT_READY terus polling |
Solving lambat atau parameter tidak valid | Verifikasi sitekey dan pageurl; tingkatkan batas polling |
| WebView crash di halaman CAPTCHA | Memory issue dengan halaman berat | Gunakan flutter_inappwebview dengan useHybridComposition: true di Android |
Pertanyaan Umum
Haruskah saya menggunakan webview_flutter atau flutter_inappwebview?
webview_flutter mencakup sebagian besar kasus. Gunakan flutter_inappwebview saat Anda memerlukan manajemen cookie, request intercepting, atau evaluasi JavaScript langsung dengan nilai return.
Bisakah saya solve CAPTCHA tanpa server backend?
Anda dapat memanggil CaptchaAI langsung dari Dart, tetapi ini mengekspos API key Anda di biner aplikasi. Selalu routing melalui backend untuk aplikasi produksi.
Bagaimana cara menangani expiry token CAPTCHA di Flutter?
Token reCAPTCHA v2 expire dalam ~120 detik. Lacak kapan token diperoleh dan solve ulang jika pengguna belum submit form dalam window tersebut.
Apakah ini berfungsi di Android dan iOS?
Ya. webview_flutter dan flutter_inappwebview mendukung Android dan iOS. Injeksi JavaScript dan komunikasi channel bekerja identik di kedua platform.
Artikel Terkait
- Cara Solve Callback reCAPTCHA v2 via API
- Penanganan reCAPTCHA v2 dan Turnstile di Situs yang Sama
- Ekstraksi Sitekey Cloudflare Turnstile
Langkah Selanjutnya
Mulai solve CAPTCHA di aplikasi Flutter Anda — dapatkan API key CaptchaAI Anda dan hubungkan backend solver Anda.
Panduan terkait:
- React Native WebView CAPTCHA Solving
- Penanganan CAPTCHA dalam Otomatisasi Mobile App dengan Appium