Tutorials

Tek Sayfalı Uygulamalarda reCAPTCHA: Dinamik Yükleme Modelleri

Playwright betiğiniz sayfayı açıyor, iframe[src*='recaptcha'] selektörünü arıyor ve zaman aşımına uğruyor. Aynı adresin kaynağını curl ile çektiğinizde ne data-sitekey ne de reCAPTCHA betiği var — oysa tarayıcıda widget tam ortada. Sorun selektörünüzde değil: React, Vue veya Angular ile yazılmış bir arayüzde reCAPTCHA ilk HTML'e hiç girmez. Bileşen bağlandığında ya da kullanıcı bir düğmeye dokunduğunda JavaScript tarafından sonradan basılır.

Bu yüzden tek sayfalı uygulamalarda çözüm akışı üç noktada klasik formlardan ayrılır: widget'ı beklemek, sitekey'i sayfa kaynağının dışından okumak ve token'ı bileşen kendini yeniden çizmeden doğru saniyede yerleştirmek. Aşağıdaki dört adım, Python (Playwright) ve Node.js (Puppeteer) örnekleriyle tam olarak bunu yapıyor.

Klasik sayfa ile SPA arasında ne değişir?

Nerede Klasik form sayfası SPA arayüzü
reCAPTCHA betiği İlk HTML yanıtında gelir Rota değişiminden sonra enjekte edilir
Widget Sayfa yüklenirken çizilir Bileşen bağlandığında (mount) çizilir
sitekey Sayfa kaynağından okunur JavaScript paketinde veya iframe adresinde
Form gönderimi POST ile tam sayfa yenilemesi XHR/fetch ile arka planda
Token'ın ömrü Sayfa yenilenene kadar durur Bileşen yeniden çizilirse anında silinir

Son satır otomasyonu en çok yanıltan davranıştır: token'ı gizli alana yazarsınız, React state'i günceller, alan boşalır ve istek token'sız gider. Enjeksiyon anı, çözüm süresinden daha belirleyicidir.

Akışın tamamı dört adımdan oluşuyor:

  1. Widget'ı ortaya çıkaran kullanıcı eylemini taklit edin.
  2. iframe veya [data-sitekey] DOM'a girene kadar bekleyin.
  3. sitekey'i çıkarıp CaptchaAI'ye gönderin, sonucu sorgulayın.
  4. Token'ı formu göndermeden hemen önce enjekte edin ya da callback'i çağırın.

Adım 1: Widget DOM'a girene kadar bekleyin

page.goto() sonrası hemen selektör aramak bir SPA'da neredeyse her zaman erkendir. Önce widget'ı görünür kılan eylemi tekrarlayın — giriş sekmesine tıklamak, sepeti onaylamak, rota değiştirmek — sonra iframe'i bekleyin. 15 saniyelik zaman aşımı yavaş bağlantılar için makul bir üst sınırdır.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://staging.example.com/qa-login")

    # SPA may need a click or navigation to trigger reCAPTCHA
    page.click("#show-login-form")

    # Wait for reCAPTCHA iframe to appear
    recaptcha_frame = page.wait_for_selector(
        "iframe[src*='recaptcha']",
        timeout=15000
    )
    print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))

Geliştirme sırasında headless=False kullanın: widget'ın tam olarak ne zaman çizildiğini kendi gözünüzle görürsünüz.

Adım 2: sitekey'i üç ayrı kaynaktan çıkarın

sitekey bir SPA'da tek bir yerde durmaz; sırayla deneyin. iframe adresindeki k parametresi en güvenilir kaynaktır, [data-sitekey] özniteliği ikinci sıradadır. JavaScript paketini taramak ise son çare olmalı: paket her derlemede değişebilir.

# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None

# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
    "[data-sitekey]",
    "el => el.getAttribute('data-sitekey')"
)

# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
    () => {
        // Check for grecaptcha render calls
        const scripts = document.querySelectorAll('script');
        for (const s of scripts) {
            const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
            if (match) return match[2];
        }
        return null;
    }
""")

Çıkardığınız 6L... değeri herkese açıktır; gizli bir bilgi değildir.

Adım 3: Çözüm görevini CaptchaAI'ye gönderin

Buradan sonrası SPA'ya özel değil. reCAPTCHA v2 için method=userrecaptcha, googlekey ve pageurl parametreleriyle in.php'ye istek gönderir, ardından res.php'yi sorgularsınız. pageurl alanına kullanıcının o an gördüğü rotayı yazın, ilk açılış adresini değil.

import requests
import time

def solve_recaptcha(site_key, page_url):
    # Submit task
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

reCAPTCHA v2 çözümleri 60 saniyenin altında tamamlanır; üç saniyelik sorgulama aralığı gereksiz istek üretmeden yeterlidir. Aynı anda kaç formu test edebileceğinizi plan thread sayınız belirler: BASIC ($15/ay, 5 thread) ile beş çözüm eşzamanlı akışta olabilir, ADVANCE ($90/ay, 50 thread) ile elli. Faturalama thread başınadır ve thread başına çözüm sayısı sınırsızdır.

Adım 4: Token'ı uygulamanın beklediği yere koyun

Klasik bir formda g-recaptcha-response alanını doldurup POST etmek yeterlidir. SPA'da bunun üç yolu var ve hangisinin çalışacağı uygulamanın kendi kodunu nasıl yazdığına bağlı.

Yöntem 1: Gizli textarea'yı doldurun

En kısa yol. Uygulama gönderim anında alanı doğrudan okuyorsa çalışır. Token'ı yazdıktan sonra araya bekleme koymadan formu gönderin.

token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")

# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

# Submit the form
page.click("#login-button")

Yöntem 2: callback fonksiyonunu elle çağırın

React ve Vue bileşenlerinin çoğu DOM'daki değeri değil, data-callback ile kaydedilen fonksiyonun ilettiği değeri kendi state'ine yazar; böyle bir uygulamada textarea'yı doldurmak hiçbir şey değiştirmez. Ayrıntı için reCAPTCHA v2 callback mekanizması yazısına bakın.

# Find the callback function name
callback = page.evaluate("""
    () => {
        const widget = document.querySelector('.g-recaptcha');
        return widget?.getAttribute('data-callback') || null;
    }
""")

# Call it with the token
if callback:
    page.evaluate(f"window['{callback}']('{token}')")
else:
    # Try the default grecaptcha callback
    page.evaluate(f"""
        if (window.grecaptcha) {{
            // Set the response and trigger verification
            document.getElementById('g-recaptcha-response').value = '{token}';
            // Find and call any registered callbacks
            const form = document.querySelector('form');
            if (form) form.dispatchEvent(new Event('submit'));
        }}
    """)

Yöntem 3: XHR'i dinleyip token'ın gittiğini doğrulayın (Puppeteer)

Form bir REST uç noktasına JSON gönderiyorsa isteği yakalayıp gövdesine bakın: token gerçekten gidiyor mu, yoksa alan boş mu — tek adımda görürsünüz.

const puppeteer = require('puppeteer');

async function solveRecaptchaInSPA() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Intercept the API call and inject the token
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('/api/login')) {
      const postData = JSON.parse(request.postData() || '{}');
      // Token already set via page.evaluate — let it pass
      request.continue();
    } else {
      request.continue();
    }
  });

  await page.goto('https://staging.example.com/qa-login');
  await page.waitForSelector('[data-sitekey]');

  const siteKey = await page.$eval(
    '[data-sitekey]',
    el => el.getAttribute('data-sitekey')
  );

  // Solve with CaptchaAI (implementation omitted — same as Python)
  const token = await solveCaptcha(siteKey, page.url());

  // Inject token and trigger callback
  await page.evaluate((t) => {
    document.getElementById('g-recaptcha-response').value = t;
    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && window[callback]) {
      window[callback](t);
    }
  }, token);
}

Framework'e göre bekleme kalıpları

Üç büyük framework aynı widget'ı farklı zamanlamalarla çizer. Beklediğiniz selektörü buna göre seçin.

React (react-google-recaptcha)

React uygulamaları çoğunlukla react-google-recaptcha paketini kullanır ve bileşen eşzamansız bağlanır. state="attached" ile beklerseniz, öğe henüz görünür olmasa bile DOM'a girdiği anda betiğiniz yoluna devam eder.

# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")

# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
    ".g-recaptcha", "el => el.dataset.sitekey"
)

Vue (vue-recaptcha)

Vue tarafında widget genellikle v-if ile koşullu çizilir; koşulu tetiklemeden beklerseniz sonuç zaman aşımıdır.

# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")

Angular

Angular projelerinde reCAPTCHA modülü tembel yüklenebilir; birden fazla olası selektörü tek beklemede toplamak sürüm farklarına dayanıklıdır.

# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")

Rota değiştiğinde widget'ı yeniden yakalayın

SPA'da gezinme tam sayfa yenilemesi yapmaz: URL değişir, sayfa nesnesi aynı kalır. Yeni rotadaki reCAPTCHA'yı yakalamak için goto yerine beklemeyi kullanın.

# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")

# Navigate within the SPA
page.click("a[href='/login']")

# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)

Bir önceki rotadan kalan token'ı yeni formda kullanmayın: yeni rota farklı bir pageurl demektir. Aynı sitede reCAPTCHA ile Turnstile yan yana çalışıyorsa iki akışı ayırın — bu senaryoyu reCAPTCHA v2 ve Turnstile'ın aynı sitede yönetimi yazısında ele alıyoruz.

Yerel senaryo: bir ödeme adımının gece regresyon testi

Türkiye'deki geliştiricilerin sık karşılaştığı SPA senaryolarından biri, Vue veya React ile yazılmış bir ödeme adımıdır: müşteri sepeti onaylar, tr-TR biçiminde adres formu açılır ve reCAPTCHA yalnızca "Siparişi Tamamla" düğmesine basıldıktan sonra çizilir. Europe/Istanbul saatiyle 03.00'te çalışan regresyon paketiniz bu adımda takılırsa sabah gelen rapor "ödeme bozuldu" der; oysa bozulan tek şey widget'ın zamanlamasıdır.

Böyle bir akışı test ederken iki noktaya dikkat edin. Test verisi gerçek müşteri kaydı olmasın: staging veritabanı ve +90 5XX XXX XX XX biçiminde üretilmiş sahte numaralar kullanın — gerçek kişisel veriyle test etmek KVKK kapsamında gereksiz risk yaratır. Maliyet tarafında çözüm başına ücretlendirme, kur dalgalanmasıyla bütçeyi öngörülemez kılar; thread bazlı sabit USD fiyat, test sayısı ikiye katlansa bile aynı kalır.

Sık karşılaşılan hatalar

Belirti Sebep Çözüm
g-recaptcha-response alanı bulunamıyor Widget henüz çizilmedi wait_for_selector'ı yeterli zaman aşımıyla kullanın
Token yazıldı ama form gönderilmiyor Callback tetiklenmedi data-callback fonksiyonunu bulup çağırın
sitekey sayfa kaynağında yok JS paketinde ya da dinamik olarak yükleniyor Widget çizilene kadar bekleyip iframe adresinden okuyun
Token bir kez çalışıp sonra reddediliyor Bileşen yeniden çizilirken alanı temizliyor Token'ı gönderimden hemen önce enjekte edin
Enjeksiyondan sonra widget sıfırlanıyor React/Vue reaktivitesi değeri siliyor textarea yerine data-callback yöntemine geçin
Token hedef sayfada reddediliyor Rota değişti, pageurl güncellenmedi Çözüm isteğine page.url() değerini yazın

Sık sorulan sorular

Token'ı enjekte ediyorum ama alan hemen boşalıyor, ne yapmalıyım?

Bileşen yeniden çizildiği için oluyor. Token'ı gönderimden hemen önce yazın veya doğrudan data-callback fonksiyonunu çağırın; ikinci yöntem değeri state'e taşıdığından reaktivite onu silmez.

SPA'da pageurl olarak hangi adresi göndermeliyim?

Kullanıcının o an gördüğü rotayı, yani page.url() çıktısını. İlk açılış adresini göndermek token'ın hedef sayfada reddedilmesine yol açar.

Aynı token'ı ikinci bir formda tekrar kullanabilir miyim?

Hayır. reCAPTCHA token'ları tek kullanımlıktır ve Google tarafında yaklaşık iki dakika sonra geçerliliğini yitirir. Her form gönderimi için yeni bir çözüm isteyin.

reCAPTCHA v3 veya Enterprise kullanan bir SPA'da akış değişir mi?

Bekleme ve enjeksiyon mantığı aynı kalır; değişen tek şey gönderdiğiniz parametrelerdir (v3 tarafında action ve beklenen skor). CaptchaAI reCAPTCHA v2, v2 Invisible, v2 Callback, v2/v3 Enterprise ve v3'ü destekler.

CaptchaAI hCaptcha'yı da çözüyor mu?

Hayır. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor; GeeTest v4 için yalnızca "çok yakında" durumu geçerli. Desteklenenler: reCAPTCHA ailesi, Cloudflare Turnstile ve doğrulama akışı, GeeTest v3, görüntü/OCR, grid, BLS; ayrıca CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta).

İlgili makaleler

Sonraki adım

Dinamik yüklenen bir reCAPTCHA'yı ilk kez çözmek beş dakikanızı alır — CaptchaAI API anahtarınızı alın ve yukarıdaki dört adımı kendi SPA'nızda deneyin.

Bu makale için yorumlar devre dışı bırakılmıştır.