渐进式 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 密钥并将动态检测集成到您的自动化中。
相关指南: