Troubleshooting

reCAPTCHA 域验证错误和修复

token 明明识别成功,服务器却返回验证失败——八成是域名验证没过,而不是识别环节的问题。reCAPTCHA 把每个 token 和生成它的域名绑定,域名对不上时 siteverify 会拒绝这个本该有效的 token,且没有明显报错。本文按错误现象拆解域名验证机制,给出可直接套用的排查与修复方法。


先看这三处检查点

reCAPTCHA 的域名验证卡在三个地方:

  1. 客户端:widget 是否加载在允许域名上
  2. token 生成:hostname 是否与页面来源一致
  3. 服务器验证:siteverify 返回的 hostname 是否匹配

reCAPTCHA 域名验证是怎么工作的

注册 reCAPTCHA 时,你要填写允许加载 widget 的域名列表(比如 example.comwww.example.com)。每次生成 token,Google 会把当前页面的 hostname 编码进 token;服务器调用 siteverify 验证时,返回的 hostname 字段就是生成 token 那一刻的真实域名:

Site owner registers reCAPTCHA → adds allowed domains (example.com, www.example.com)
    ↓
reCAPTCHA widget loads on example.com → matches allowed domain ✓
    ↓
Token generated with embedded hostname
    ↓
Server validates token via siteverify API
    ↓
Google checks: Does token hostname match allowed domains?
    ├─ YES → { "success": true, "hostname": "example.com" }
    └─ NO  → { "success": false, error or hostname mismatch }

三种最常见的域名验证错误

现象一:siteverify 返回的 hostname 对不上

{
    "success": true,
    "hostname": "subdomain.example.com",
    "challenge_ts": "2025-01-15T10:30:00Z"
}

token 有效,但 hostname 和预期域名不一致,一些服务器实现会因此直接拒绝:

# Server-side validation that checks hostname
def validate_token(token, secret_key, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret_key, "response": token},
    ).json()

    if not result.get("success"):
        return False

    # This check causes failures when hostnames don't match
    if result.get("hostname") != expected_hostname:
        return False  # Domain mismatch!

    return True

常见原因:

  1. www.example.com 上识别的 token,拿到 example.com 上验证
  2. staging.example.com 上识别的 token,拿到 example.com 上验证
  3. 反向代理或 CDN 改写了 Host(阿里云、腾讯云边缘节点后尤其常见)

修复方法: 确保 pageurl 与提交域名完全一致。

现象二:widget 直接拒绝加载

reCAPTCHA widget 报错,或者干脆不渲染:

ERROR: Invalid domain for site key

常见原因:

  1. site key 允许的域名列表里没有当前页面的域名
  2. 用 localhost 或 file:// 协议加载 widget
  3. 用 IP 地址代替域名访问页面

这是网站所有者的配置问题,脚本只需保证 pageurl 落在允许域名内。

现象三:明明识别正确,token 还是被拒绝

{
    "success": false,
    "error-codes": ["invalid-input-response"]
}

token 是为另一个域名生成的。常见原因: 发给识别接口的 pageurl 和目标域名不一致:

# WRONG: pageurl doesn't match actual target
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": "https://staging.example.com/qa-login",  # ← Must match actual domain
    "json": 1,
})

# But submitting token to:
requests.post("https://app.staging.example.com/qa-login", ...)  # Different subdomain!

排查经验:国内 CDN(阿里云、腾讯云)或反向代理容易改写 Host,这是最容易被忽略的一种。


常见问题

赶时间就先看这几条,细节见后文。

部署在阿里云、腾讯云 CDN 后面,为什么经常报 hostname 不匹配?

CDN 边缘节点转发时,有时会改写或丢失原始 Host 头,siteverify 拿到的 hostname 就和地址栏不一致。先检查 CDN 回源是否透传原始 Host,再核对 pageurl 与实际域名。

一个域名下识别成功的 token,可以在别的域名上用吗?

不可以。token 只绑定生成它的域名,example.com 上的 token 换到 other-site.com 提交必然失败;严格校验 hostname 时,连子域名互换也可能失败。

本地测试时 token 正常,上线后却总是被拒绝,为什么?

常见原因:本地用 localhost,规则和线上不同;线上多了 CDN 或负载均衡,siteverify 返回的 hostname 变了;线上校验通常更严格;测试机和线上服务器网络出口也可能不同。

多个子域名共用一个 site key,pageurl 该怎么填?

按 token 实际生成的子域名填,不要统一填主域名。比如 app.example.com 上识别出的 token,pageurl 就填 https://app.example.com/...,而不是 example.com

Turnstile 或 GeeTest 也有同样的域名绑定问题吗?

思路相通。Turnstile 和 GeeTest v3 也会把 sitekey(或 gt)和注册域名绑定,验证失败先核对 pageurl 是否一致,往往能排除大半问题。


域名到底怎么算匹配

精确匹配还是通配符

reCAPTCHA 默认不是严格按子域名匹配,取决于站点所有者的后台配置:

  1. 注册 example.comexample.comwww.example.comsub.example.com 都能通过(开启通配符时)
  2. 注册 www.example.com → 仅 www.example.com 本身能通过(严格模式下)
  3. 注册 *.example.comexample.com 下的任意子域名都能通过
  4. 注册 localhost → 仅 localhost 能通过(用于本地开发)

服务器该怎么处理 hostname

hostname 是生成 token 的页面域名,接受与否由服务器决定——宽松校验允许任意子域名,严格校验要求完全一致:

# Permissive validation (accepts any subdomain)
def validate_permissive(token, secret, base_domain):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    if not result.get("success"):
        return False

    hostname = result.get("hostname", "")
    return hostname == base_domain or hostname.endswith(f".{base_domain}")


# Strict validation (exact match only)
def validate_strict(token, secret, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    return result.get("success") and result.get("hostname") == expected_hostname

经验法则:拿不准精确匹配还是通配符,先用宽松校验起步,再按报错收紧。


四种自动化里的修复方法

方法一:让 pageurl 精确匹配目标域名

最有效的修复:确保 pageurl 与提交域名一致:

# Correct: pageurl matches where you'll submit the token
target_url = "https://www.staging.example.com/qa-login"

submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": target_url,  # Must match the actual domain
    "json": 1,
})

方法二:处理 www 与非 www 的差异

from urllib.parse import urlparse

def normalize_url(url):
    """Normalize URL for consistent domain matching."""
    parsed = urlparse(url)
    # Use exactly what the target site uses
    # Check if the site redirects www → non-www or vice versa
    return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"

# Test which variant the site uses
response = requests.get("https://staging.example.com/qa-login", allow_redirects=True)
actual_url = response.url  # May be https://www.staging.example.com/qa-login after redirect

方法三:跟着重定向链找到真实域名

有些站点会多次跳转,最终域名和最初访问的不同:

def get_final_url(url):
    """Follow redirects to find the actual CAPTCHA page domain."""
    response = requests.get(url, allow_redirects=True, timeout=15)
    return response.url

# Login URL might redirect:
# https://staging.example.com/qa-login → https://auth.staging.example.com/qa-login
final_url = get_final_url("https://staging.example.com/qa-login")
# Use final_url as pageurl for solver

方法四:从 reCAPTCHA 回调地址里提取域名

from bs4 import BeautifulSoup
from urllib.parse import urlparse

def extract_recaptcha_domain(html, page_url):
    """Extract the domain reCAPTCHA uses for token binding."""
    soup = BeautifulSoup(html, "html.parser")

    # Check for reCAPTCHA iframe
    iframe = soup.find("iframe", src=lambda s: s and "recaptcha" in s)
    if iframe:
        src = iframe.get("src", "")
        # The iframe URL may contain the domain parameter
        if "domain=" in src:
            # Extract domain from iframe URL
            pass

    # Default: use the page URL's domain
    return urlparse(page_url).netloc

小结:方法一通常已经够用,其余三种方法应对更复杂的重定向场景。


动手改代码前,先跑一遍诊断脚本

四种方法都要求先知道真实域名,不如先用脚本把重定向链和 www 变体跑一遍:

import requests
from urllib.parse import urlparse

class DomainDiagnostic:
    """Diagnose domain verification issues for reCAPTCHA solving."""

    def __init__(self, target_url):
        self.target_url = target_url
        self.issues = []

    def check_redirects(self):
        """Check if the URL redirects to a different domain."""
        try:
            response = requests.get(
                self.target_url, allow_redirects=True, timeout=15,
                headers={"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0"},
            )
            final_url = response.url
            original_domain = urlparse(self.target_url).netloc
            final_domain = urlparse(final_url).netloc

            if original_domain != final_domain:
                self.issues.append({
                    "type": "redirect",
                    "message": f"Redirects from {original_domain} to {final_domain}",
                    "fix": f"Use pageurl: {final_url}",
                })

            return final_url
        except Exception as e:
            self.issues.append({"type": "error", "message": str(e)})
            return self.target_url

    def check_www_variant(self):
        """Check if www and non-www point to the same content."""
        parsed = urlparse(self.target_url)
        domain = parsed.netloc

        if domain.startswith("www."):
            alt_domain = domain[4:]
        else:
            alt_domain = f"www.{domain}"

        alt_url = self.target_url.replace(domain, alt_domain)

        try:
            alt_response = requests.get(alt_url, allow_redirects=True, timeout=10)
            alt_final = urlparse(alt_response.url).netloc

            if alt_final != domain and alt_final != alt_domain:
                self.issues.append({
                    "type": "www_redirect",
                    "message": f"{alt_domain} redirects to {alt_final}",
                })
        except Exception:
            pass

    def report(self):
        """Generate diagnostic report."""
        final_url = self.check_redirects()
        self.check_www_variant()

        print(f"Target URL: {self.target_url}")
        print(f"Final URL:  {final_url}")
        print(f"Use as pageurl: {final_url}")

        if self.issues:
            print("\nIssues found:")
            for issue in self.issues:
                print(f"  [{issue['type']}] {issue['message']}")
                if "fix" in issue:
                    print(f"  Fix: {issue['fix']}")
        else:
            print("\nNo domain issues detected.")


# Usage
diag = DomainDiagnostic("https://staging.example.com/qa-login")
diag.report()

跑一遍就能分清是验证逻辑问题还是网络链路问题。

提示:API Key 放环境变量,不要写死在脚本里。


故障排查对照表

现象 可能原因 怎么诊断 怎么处理
token 一直被拒绝 pageurl 和目标域名不一致 把识别接口用的 pageurl 和实际提交域名做对比 更新 pageurl,让两者一致
www 能过、非 www 不行 域名变体不一致 检查重定向行为 跟着重定向,使用目标站点实际用的那个变体
有时候过、有时候不过 CDN 或负载均衡返回的域名不固定 检查域名是否随请求变化 统一用重定向链里那个稳定的 URL
浏览器里能过、脚本里不行 脚本请求来源和浏览器不一致 对比浏览器地址栏和脚本里的 pageurl 让脚本用浏览器最终停留的那个 URL
Enterprise token 被拒 项目或域名绑定错了 核对 Enterprise site key 绑定的域名 去 Enterprise 控制台检查域名配置

总结

reCAPTCHA 域名验证的本质,是把 token 和生成它的 hostname 绑在一起。自动化脚本最常踩的坑,是发给 CaptchaAI 识别接口的 pageurl 和最终提交域名对不上。跟着重定向链找真实域名、处理好 www 差异,先诊断再改代码,基本能覆盖本文场景。

相关文章

该文章已禁用评论。