实战教程

渐进式 Web 应用程序 (PWA) 中的验证码处理

渐进式 Web 应用程序提出了独特的验证码挑战。它们使用客户端渲染、Service Workers 和单页导航 - 这意味着验证码会动态加载,而不是使用初始 HTML。CaptchaAI处理解决方案,但您需要正确的检测策略来捕获页面加载后注入 DOM 的验证码。

本指南涵盖使用 Playwright 和 CaptchaAI 在 PWA 环境中检测、提取和解决验证码。

为什么 PWA 不同

传统网站在初始 HTML 响应中提供验证码。 PWA 在几个关键方面有所不同:

方面 传统网站 渐进式网页应用
验证码加载 在初始 HTML 中 页面加载后由 JavaScript 渲染
页面导航 整页重新加载 客户端路由(无需重新加载)
服务人员 不存在 缓存资源,可能拦截请求
DOM 可用性 即时 框架渲染后
网络请求 直接的 可能被 Service Worker 拦截

第1步:等待动态验证码渲染

最大的错误是尝试在 PWA 框架呈现 CAPTCHA 小部件之前提取站点密钥。使用突变观察者或特定于框架的信号:

// 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 with 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);

第 2 步:处理 Service Worker 缓存

Service Worker 可以缓存验证码脚本,从而导致小部件过时。需要时绕过缓存:

// Intercept and QA tanılama 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' } });
});

第 3 步:处理客户端导航

PWA 使用客户端路由 - 导航到受验证码保护的路由不会触发页面加载。监视路由更改:

// 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);

第 4 步:使用 Selenium 替代 Python

# 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()

令牌无效检查

  • 当路由更改、身份验证状态更改或服务工作人员刷新更改页面上下文时,使缓存的 CAPTCHA 令牌失效。
  • 仅存储将已解决的令牌重新连接到活动视图所需的最小状态。
  • 在提交之前验证令牌的新鲜度,而不是假设后台解决仍然有效。

故障排除

问题 原因 处理方式
验证码元素永远不会出现 PWA 尚未渲染路线 使用waitForSelector并延长超时时间;确保客户端路由完成
导航后失效的站点密钥 Service Worker 提供缓存的 HTML 绕过验证码相关资源的缓存标头
未找到token 提交回调 PWA 框架以不同的方式管理状态 检查React/Vue/Angular状态管理;触发表单状态更新
表单提交不发送令牌 SPA 表单处理程序从组件状态读取,而不是 DOM 还更新框架的状态(例如,React ref、Vue React 属性)

常问问题

PWA 使用与常规网站不同的验证码类型吗?

不会。PWA 使用相同的 reCAPTCHA、Turnstile 和其他 CAPTCHA 小部件。区别在于时间——它们是动态加载的。

Service Workers 可以阻止验证码解决吗?

Service Worker 可以缓存 CAPTCHA 脚本,但您可以通过缓存清除标头或在自动化过程中禁用 Service Worker 来绕过此功能。

CaptchaAI 处理 PWA 特定令牌是否有所不同?

不会。无论验证码是在 PWA 还是传统页面中提供,令牌都是相同的。 CaptchaAI 使用站点密钥和 URL — 两者是相同的。

相关文章

下一步

开始解决 PWA 中的验证码 -”获取您的 CaptchaAI API 密钥并将动态检测集成到您的自动化中。

相关指南:

该文章已禁用评论。