Integrasi

Penanganan CAPTCHA di Flutter WebViews dengan CaptchaAI

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:

  1. Deteksi widget CAPTCHA setelah WebView dimuat
  2. Ekstrak sitekey via JavaScript channel
  3. Solve via CaptchaAI dari layanan backend
  4. 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:

Komentar dinonaktifkan untuk artikel ini.