Tutoriales

Manejo de CAPTCHA en aplicaciones web progresivas (PWA)

Las aplicaciones web progresivas presentan desafíos CAPTCHA únicos. Utilizan renderizado del lado del cliente, Service Workers y navegación de una sola página, lo que significa que los CAPTCHA se cargan dinámicamente en lugar de con el HTML inicial.CaptchaAImaneja la resolución, pero necesita la estrategia de detección correcta para detectar CAPTCHA que se inyectan en el DOM después de que se carga la página.

Esta guía cubre la detección, extracción y resolución de CAPTCHA en contextos PWA con Playwright y CaptchaAI.

Por qué las PWA son diferentes

Los sitios web tradicionales ofrecen CAPTCHA en la respuesta HTML inicial. Las PWA se diferencian en varios aspectos clave:

Aspecto Sitio Tradicional PWA
Cargando CAPTCHA En HTML inicial Representado por JavaScript después de cargar la página
Navegación de página recarga de página completa Enrutamiento del lado del cliente (sin recarga)
Trabajador de servicio No presente Almacena en caché los recursos, puede interceptar solicitudes
Disponibilidad DOM Inmediato Después de renderizar el marco
Solicitudes de red directo Puede ser interceptado por el trabajador del servicio.

Paso 1: Espere la representación CAPTCHA dinámica

El mayor error es intentar extraer las claves del sitio antes de que el marco PWA haya representado el widget CAPTCHA. Utilice observadores de mutaciones o señales específicas del marco:

// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function detectCaptchaInPWA(page) {
  // Wait for the PWA app shell to render
  await page.waitForLoadState('networkidle');

  // Use MutationObserver to detect dynamically loaded CAPTCHAs
  const captchaInfo = await page.evaluate(() => {
    return new Promise((resolve) => {
      // Check if CAPTCHA is already present
      const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
      if (existing) {
        resolve({
          type: existing.classList.contains('g-recaptcha')
            ? 'recaptcha_v2' : 'turnstile',
          sitekey: existing.getAttribute('data-sitekey'),
          pageurl: window.location.href,
        });
        return;
      }

      // Watch for CAPTCHA elements added dynamically
      const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeType !== 1) continue;
            const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
              ? node
              : node.querySelector?.('.g-recaptcha, .cf-turnstile');
            if (captcha) {
              observer.disconnect();
              resolve({
                type: captcha.classList.contains('g-recaptcha')
                  ? 'recaptcha_v2' : 'turnstile',
                sitekey: captcha.getAttribute('data-sitekey'),
                pageurl: window.location.href,
              });
              return;
            }
          }
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      // Timeout after 15 seconds
      setTimeout(() => {
        observer.disconnect();
        resolve(null);
      }, 15000);
    });
  });

  return captchaInfo;
}

async function main() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example-pwa.com/login');

  const captcha = await detectCaptchaInPWA(page);

  if (!captcha) {
    console.log('No CAPTCHA detected');
    await browser.close();
    return;
  }

  console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);

  // Solve con CaptchaAI
  const token = await solveCaptcha(captcha);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Inject token
  await injectToken(page, captcha.type, token);

  // Submit form
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle' });

  console.log('Form submitted');
  await browser.close();
}

async function solveCaptcha(captcha) {
  const params = {
    key: API_KEY,
    pageurl: captcha.pageurl,
    json: '1',
  };

  if (captcha.type === 'recaptcha_v2') {
    params.method = 'userrecaptcha';
    params.googlekey = captcha.sitekey;
  } else {
    params.method = 'turnstile';
    params.sitekey = captcha.sitekey;
  }

  const submit = await axios.get(
    'https://ocr.captchaai.com/in.php', { params }
  );
  if (submit.data.status !== 1) throw new Error(submit.data.request);

  const taskId = submit.data.request;

  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      throw new Error(poll.data.request);
    }
  }
  throw new Error('Timeout');
}

async function injectToken(page, type, token) {
  if (type === 'recaptcha_v2') {
    await page.evaluate((t) => {
      document.getElementById('g-recaptcha-response').value = t;
      try {
        const clients = ___grecaptcha_cfg.clients;
        Object.keys(clients).forEach((k) => {
          Object.keys(clients[k]).forEach((j) => {
            if (clients[k][j]?.callback) clients[k][j].callback(t);
          });
        });
      } catch (e) {}
    }, token);
  } else {
    await page.evaluate((t) => {
      const input = document.querySelector('[name="cf-turnstile-response"]');
      if (input) input.value = t;
      const cb = document.querySelector('.cf-turnstile')
        ?.getAttribute('data-callback');
      if (cb && typeof window[cb] === 'function') window[cb](t);
    }, token);
  }
}

main().catch(console.error);

Paso 2: Manejar el almacenamiento en caché del trabajador de servicio

Los trabajadores de servicios pueden almacenar en caché los scripts CAPTCHA, lo que genera widgets obsoletos. Omita el caché cuando sea necesario:

// Intercept and omisión Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

await page.route('**/turnstile/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

Paso 3: Manejar la navegación del lado del cliente

Las PWA utilizan enrutamiento del lado del cliente: navegar a una ruta protegida por CAPTCHA no activa la carga de la página. Supervise los cambios de ruta:

// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
  const originalPushState = history.pushState;
  history.pushState = function() {
    originalPushState.apply(this, arguments);
    window.dispatchEvent(new Event('pwa-route-change'));
  };
});

page.on('console', async (msg) => {
  // React to route changes if needed
});

// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);

Cuándo invalidar el token actual

Cambio en la PWA Reutilizar token Volver a resolver
Navegación dentro de la misma vista y misma sesión A veces Solo si el sitio no recrea el widget
Cambio de ruta con nuevo widget No Sí, porque suele cambiar el sitekey o el callback
Refresco del service worker o hard reload No Sí, el estado anterior deja de ser fiable

Paso 4: Alternativa de Python con Selenium

# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

API_KEY = "YOUR_API_KEY"

driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")

# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)

sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")

# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
    params["method"] = "turnstile"
    params["sitekey"] = sitekey
else:
    params["method"] = "userrecaptcha"
    params["googlekey"] = sitekey

resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]

# Poll
for _ in range(30):
    time.sleep(5)
    poll = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1",
    })
    if poll.json().get("status") == 1:
        token = poll.json()["request"]
        break
else:
    raise TimeoutError("CAPTCHA not solved")

# Inject token
driver.execute_script(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()

Solución de problemas

problema causa Solución
El elemento CAPTCHA nunca aparece PWA aún no ha renderizado la ruta Utilice waitForSelector con tiempo de espera extendido; garantizar que se complete el enrutamiento del lado del cliente
Clave de sitio obsoleta después de la navegación Service Worker sirvió HTML en caché Omitir encabezados de caché para recursos relacionados con CAPTCHA
No se encontró la devolución de llamada de inyección de token El marco PWA gestiona el estado de manera diferente Verifique la gestión del estado de React/Vue/Angular; activar la actualización del estado del formulario
El envío del formulario no envía el token El controlador de formulario SPA lee desde el estado del componente, no desde DOM Actualice también el estado del marco (por ejemplo, React ref, propiedad reactiva de Vue)

Preguntas frecuentes

¿Las PWA utilizan diferentes tipos de CAPTCHA que los sitios normales?

No. Las PWA utilizan los mismos widgets reCAPTCHA, Turnstile y otros CAPTCHA. La diferencia es el tiempo: se cargan dinámicamente.

¿Pueden los Service Workers bloquear la resolución de CAPTCHA?

Los Service Workers pueden almacenar en caché los scripts CAPTCHA, pero puedes evitar esto con encabezados que eliminan la caché o deshabilitando el Service Worker durante la automatización.

¿CaptchaAI maneja los tokens específicos de PWA de manera diferente?

No. Los tokens son idénticos ya sea que el CAPTCHA se haya entregado en una PWA o en una página tradicional. CaptchaAI utiliza la clave del sitio y la URL; ambas son iguales.

Artículos relacionados

Próximos pasos

Comience a resolver CAPTCHA en PWA:obtenga su clave API CaptchaAIe integre la detección dinámica en su automatización.

Guías relacionadas:

  • Carga dinámica de CAPTCHA: detección de desafíos cargados de forma diferida
  • Cloudflare Turnstile Extracción de claves de sitio
  • Extracción de parámetros reCAPTCHA de la fuente de la página
Los comentarios están deshabilitados para este artículo.