AI 爬虫被 CDN 拦在门外:Cloudflare 与云 WAF 的 UA 放行规范,以及 403 排查清单

2026-09-17 11:40:59 13 次浏览
CloudflareWAFBot 管理robots.txtAI 爬虫

适用读者:负责外贸独立站或海外版官网的运维、后端与 SEO 工程师。涉及 CDN / WAF 配置、日志分析与 robots.txt 授权策略,平台以 Cloudflare、阿里云国际站与 Nginx 自建为例。

一个做了六周的外贸站,页面对比测评都写好了,sitemap 交了,robots.txt 里 User-agent: * 全部 Allow。客户问我们:为什么在 AI 里搜品牌名,出来的还是三年前的旧描述?我们翻访问日志,六周内来自 AI 抓取器的请求一共 3 条,其余全是被 403 挡回的记录。

问题不在内容,在门口。CDN 的机器人管理默认开着,把不认识的 UA 当恶意流量处理了。

三层拦截面:先搞清楚是谁在挡

外网站的"可抓取性"不是一道门,是三道。日志里只看到 403,很容易归因错。

拦截面 判定依据 被拦时的表现 排查入口
网络与边界层 源站防火墙、区域封锁、恶意 IP 库 连接超时或 444,日志里常常什么都没有 云厂商安全组、fail2ban、WAF 全量日志
CDN 与机器人管理层 UA 黑白名单、TLS 指纹(JA3/JA4)、速率与行为评分 403 / 429,返回 CDN 自带拦截页 CDN 后台的 Bot 管理、安全事件视图
应用与协议层 应用防火墙规则、robots.txt、登录墙、参数校验 403 / 401 / 200 但内容为空 应用日志、robots.txt、渲染结果

我们这次的 403 落在第二层。第一层没问题,第三层也没有——robots.txt 是放行的,恰恰因为只看了这一层,前面六周没人往 CDN 后台看。

flowchart TD
    A[AI 抓取器发起请求] --> B{边界层放行}
    B -->|超时/444| X[请求根本进不来]
    B -->|通过| C{CDN 机器人管理判定}
    C -->|UA 不在白名单| Y[403 CDN 拦截页]
    C -->|速率触发阈值| Z[429 或挑战页]
    C -->|通过| D{应用与协议层}
    D -->|robots 禁止| E[被合规爬虫主动放弃]
    D -->|登录墙/参数校验| F[401 或空内容]
    D -->|全部通过| G[返回 HTML 供抽取]

原理剖析:CDN 凭什么认定"你是机器人"

要把这事调对,得知道判定是怎么做出来的。主流 CDN 的机器人识别通常叠几个信号,任何一个都可能把合法抓取器判成坏人。

TLS 指纹。 客户端建立 TLS 时协商的密码套件顺序、扩展字段组合,构成一个指纹(JA3,较新的是 JA4)。同一个 UA 声称自己是某厂家的抓取器,但 TLS 指纹是一个普通 Python 脚本,两者对不上,评分就会掉。

IP 归属验证。 正规厂商会在官方文档里公布自己的出口 IP 段。CDN 可以拿这个名单反查:UA 写着 GPTBot、IP 却来自某个共享机房,就按"伪造者"处理。这一步最容易误伤的是自建在云服务器上的抓取器,以及通过企业代理出口的合法抓取。

UA 一致性。 UA 缺失、UA 里版本号与其他请求矛盾(同一 IP 上一秒是手机浏览器,下一秒是抓取器),都会触发降分。

行为评分。 单位时间的请求速率、是否只取少量 URL 却请求频繁、是否命中已知的攻击路径特征。外贸站有个天然劣势:站点总 URL 数少,抓取器一轮就把站刷完了,速率阈值很容易被踩到。

sequenceDiagram
    participant C as AI 抓取器
    participant W as CDN 边缘
    participant O as 源站
    C->>W: TLS 握手(JA3 指纹)
    C->>W: GET /product/gear-x,UA 自称 GPTBot
    W->>W: 指纹评分 + UA 解析 + IP 段比对
    alt 评分低于阈值
        W-->>C: 403 CDN 拦截页
    else 评分通过
        W->>O: 转发请求
        O-->>W: 200 HTML
        W-->>C: 200(可能被缓存)
    end
    Note over W,C: 被拦记录只出现在 CDN 日志,源站看不到

这解释了一个反直觉的现象:源站日志越干净,越可能有问题。如果源站访问日志里几乎看不到抓取器,同时 CDN 侧有大量 403,那就是被挡在门外,而不是"没人来抓"。

规范解读:robots.txt 是授权声明,不是通行证

robots.txt 的规范是 RFC 9309,它约束的是爬取方——合规的抓取器读到 Disallow 会主动放弃。它不约束服务端,也不能让任何请求穿过后面的技术拦截层。

于是有了四种组合,只有第一种是想要的状态:

robots.txt CDN / WAF 实际结果 处理方向
Allow 放行 正常抓取与索引 保持,并把变更通知渠道接上
Allow 拦截 授权了却进不来,最隐蔽的一种 打开 CDN 机器人管理,按需放行
Disallow 放行 合规抓取器不来,违规采集照样来 补 robots 声明,并在边缘层做速率限制
Disallow 拦截 预期内,但要确认没误伤自家监控 复核白名单

注意第一行和第三行的对比:想拦住的是违规采集,而 robots.txt 只能约束守规矩的那批。真正的防采集要靠速率限制和行为判定,不该靠给 AI 抓取器一刀切。

在 CDN 上放行时,规则写法的粒度也值得留意。用 UA 包含匹配虽然简单,但 UA 是客户端自报的,伪造成本为零——所以放行规则应该和"官方 IP 段"或"已验证机器人"机制配合使用,而不是单独用 UA 做信任判断。生产环境的折中做法是:对匹配官方 IP 段的请求走高信任通道,其余走限速通道

落地:Cloudflare 与 Nginx 的放行写法

环境:Cloudflare 免费版以上(Bot Fight Mode 与 WAF 自定义规则可用),源站 Nginx 1.24,测试用 curl 7.x。

先在 CDN 侧加一条"跳过"规则,让已知 AI 抓取器不被机器人管理处理:

# Cloudflare WAF 自定义规则(表达式写法)
# 命中条件:UA 命中已知 AI 抓取器之一,且请求方法为 GET/HEAD
# 只放 GET/HEAD:抓取器不应带 POST,带上就该被拦
(http.user_agent contains "GPTBot" or
 http.user_agent contains "ClaudeBot" or
 http.user_agent contains "PerplexityBot" or
 http.user_agent contains "Google-Extended")
and http.request.method in {"GET" "HEAD"}
# 动作:Skip —— 跳过下列安全功能(不要选 Block / Managed Challenge)
#   1) Bot Fight Mode
#   2) 速率限制规则中的"挑战"动作
# 注意:Skip 只是不拦,不会绕过源站自身的鉴权

源站这侧再加一层,把 CDN 已经判定过的身份透传下来,避免重复判定:

# nginx.conf 片段:只允许来自 CDN 回源段的请求直连,其余拒绝
# 说明:CDN 回源段取自厂商官方文档,务必定期更新,写死会随厂商扩容失效
geo $from_cdn {
    # 默认 0 表示非 CDN 来源;下面的网段必须与厂商文档保持同步
    default        0;
    173.245.48.0/20 1;   # 示例网段,实际以 Cloudflare 官方 IP 列表为准
    103.21.244.0/22 1;   # 示例网段,实际以 Cloudflare 官方 IP 列表为准
}

server {
    # 只开 443;80 端口交给 CDN 做跳转,源站不再直接服务明文请求
    listen 443 ssl http2;

    # 非 CDN 来源的直接访问一律拒绝:防止有人绕过 CDN 打源站
    if ($from_cdn = 0) {
        return 403;
    }

    # 透传真实客户端 IP,日志里才能看到抓取器的真实地址
    real_ip_header CF-Connecting-IP;
    set_real_ip_from 173.245.48.0/20;    # 与上面的回源段保持一致

    location / {
        # 静态资源交回 Nginx,正文交给上游应用渲染
        # 抓取器走正常渲染,不做 UA 层限速
        # 限速针对的是采集特征,不是身份:这里只对高频无 UA 请求限流
        limit_req zone=no_ua burst=20 nodelay;
        proxy_pass http://app_upstream;
    }
}

放行之后要验证,别只看规则界面显示"已生效"。用 curl 直接带 UA 打一次,跟不带 UA 的做对照:

# 环境:curl 7.x、站点已挂 CDN
# 1) 带官方抓取器 UA 请求,看是否还是 403 拦截页
curl -s -o /dev/null -w 'with_ua=%{http_code}\n' \
  -A "GPTBot/1.0 (+https://example.com/gptbot)" https://www.example.com/product/gear-x

# 2) 不带 UA 请求,作为对照组;这一步返回 403 属于预期行为
curl -s -o /dev/null -w 'no_ua=%{http_code}\n' https://www.example.com/product/gear-x

# 3) 看响应体是不是 CDN 的拦截页:拿到 HTML 才说明真正回源了
curl -s -A "GPTBot/1.0" https://www.example.com/product/gear-x | head -5

第三步是关键。只看状态码不够:部分 CDN 的挑战页会返回 200,但内容是脚本跳转页,抓取器拿不到正文。

从日志反推拦截:一份可复用的排查顺序

排查顺序很重要,从上层往下走,避免在源站里空找。我们的顺序是:CDN 安全事件视图 → CDN 边缘日志 → 源站访问日志 → 应用日志 → 渲染结果。

下面这段脚本用来把 CDN 日志里的抓取器请求和被拦请求分开统计:

# 环境:Python 3.10+,仅标准库;输入为 CDN 边缘日志导出文件(JSON Lines)
# 作用:区分"被拦"与"正常抓取",并统计被拦最多的 UA
# 输出:四类计数 + 被拦 UA / 路径的 Top 榜,直接贴群里就能对齐认知
import json
from collections import Counter

# 官方文档公布的出口 IP 段(示例占位,实际请从厂商文档拉取最新列表)
TRUSTED_PREFIXES = ("20.42.", "172.203.", "52.230.")

# 已知 AI 抓取器的 UA 关键字:只做归类,不用来判定信任
AI_UA_HINTS = ("GPTBot", "ClaudeBot", "PerplexityBot", "Google-Extended", "CCBot")


def classify(rec: dict) -> str:
    ua = rec.get("ClientRequestUserAgent", "") or ""
    ip = rec.get("ClientIP", "") or ""
    status = int(rec.get("EdgeResponseStatus", 0) or 0)

    # UA 命中已知抓取器:只说明它自称是谁,不代表可信
    hit_ai = any(h in ua for h in AI_UA_HINTS)
    # IP 落在官方公布的出口段内:这一条才是信任的来源
    ip_trusted = ip.startswith(TRUSTED_PREFIXES)

    if hit_ai and not ip_trusted:
        return "ua_only_pending_verify"   # UA 像抓取器但 IP 不在名单,需要人工看
    if hit_ai and ip_trusted and status in (403, 429, 503):
        return "blocked_official_crawler"  # 真正要处理的一类:官方抓取器被拦
    # 正常抓取:UA 与 IP 都符合,状态码 200
    if hit_ai and status == 200:
        return "ok"
    # 其余归为 other:包含普通用户流量与无 UA 采集,先不细分
    return "other"


def main(path_log: str) -> None:
    buckets = Counter()
    blocked_ua = Counter()   # 被拦请求的 UA 分布
    blocked_url = Counter()  # 被拦最多的路径,通常指向 cdn-cgi 或站点首页

    # errors="ignore":CDN 日志里偶发截断行,交给下面的 try 兜住即可
    with open(path_log, encoding="utf-8", errors="ignore") as f:
        for line in f:
            try:
                rec = json.loads(line)
            except Exception:
                continue          # 非 JSON 行直接跳过,别让坏行中断统计
            tag = classify(rec)
            buckets[tag] += 1
            if tag == "blocked_official_crawler":
                blocked_ua[(rec.get("ClientRequestUserAgent") or "")[:40]] += 1
                blocked_url[(rec.get("ClientRequestURI") or "")[:60]] += 1

    # 先看总量分布,再看具体是谁被拦
    for k, v in buckets.most_common():
        print(f"{v:>7}  {k}")
    print("\n被拦 UA 分布")
    for ua, v in blocked_ua.most_common(10):
        print(f"{v:>7}  {ua}")
    print("\n被拦路径分布")
    for u, v in blocked_url.most_common(10):
        print(f"{v:>7}  {u}")


if __name__ == "__main__":
    main("cdn_edge_log.jsonl")

这段代码里有个判断值得强调:ua_only_pending_verify 这一类不要自动加白。UA 可以随便写,把 UA 当成身份凭证等于给伪造者开门。这一类应该走人工核对,或者接入厂商的已验证机器人机制。

修复前后我们观察到的变化

改动本身很小:一条跳过规则、一段回源段配置、一次 nginx reload。观测窗口四周。

观测项 处理前(连续 4 周) 处理第 1 周 处理第 4 周
CDN 边缘日志中 AI 抓取器请求 3 次 87 次 每周 300 次上下
其中被拦(403/429)占比 约 100% 12% 低于 3%
源站日志可见的抓取器请求 0 开始出现 与 CDN 侧数量基本对齐
站点被 AI 回答引用时描述与官网一致 仍引用旧描述 开始出现新页描述 抽检 20 个问题,15 个与官网一致

第三行是这次改动的直接证据:改动前源站日志是空的,因为请求根本没到源站。第一周被拦比例还有 12%,原因是速率限制规则还在挑战高频请求,我们把抓取器从速率挑战里也排除了之后才降下来。

三个常见判断错误

以为 robots.txt 放行就够了。 前面那张四象限表里,第二行是最容易踩的:文件里写着 Allow,读的人也以为没问题,实际请求在 CDN 就被 403 掉了,而且不会有任何报错。

以为开了机器人管理更安全。 对纯展示型官网,机器人管理带来的收益是挡掉一些采集和无意义请求;代价是把 AI 引擎的抓取一起挡掉。两件事要分开决策:采集要不要管,和 AI 抓取要不要放,是两条独立策略。

以为 403 一定是爬虫问题。 也见过反向案例:站点用了接口鉴权,页面 HTML 里数据靠带 token 的 XHR 拿,抓取器拿到的是空壳。这类问题的表现同样是"AI 看不到内容",但归属第三层,得从渲染方式下手,跟 CDN 规则无关。

往后会怎么变

可以预见的两个方向。一是抓取器身份会往"可验证"走:厂商公布出口 IP 段、并在 robots.txt 之外引入签名或凭证机制,服务端才知道眼前这个自称 GPTBot 的请求到底是不是本尊。二是放行策略会从"按 UA 一刀切"变成"按身份分级":已验证抓取器走无挑战通道,来源不明的走限速通道,违规的才拒绝。

在此之前,我的建议是每次动 CDN 安全策略都留一份对照记录:改之前抓一次带 UA 的请求、改之后抓一次,把状态码和响应体头几行存下来。这份记录在半年后排查"AI 怎么突然不引用了"时,比任何配置界面截图都有用。

评论区欢迎贴你们 CDN 里的抓取器放行规则表达式,尤其是云厂商那套"已验证机器人"的写法,各家差别挺大。

参考与延伸

  • robots.txt 规范(RFC 9309):https://www.rfc-editor.org/rfc/rfc9309
  • OpenAI 爬虫与出口 IP 段说明:https://platform.openai.com/docs/bots
  • Anthropic 爬虫说明(ClaudeBot):https://support.anthropic.com/en/articles/8896518
  • Cloudflare 机器人管理与 WAF 自定义规则文档:https://developers.cloudflare.com/waf/
  • Nginx 的 real_ip 与限流模块文档:https://nginx.org/en/docs/http/ngx_http_realip_module.html

关键词:GEO、AI优化AIO、AI 爬虫、Cloudflare Bot 管理、WAF 放行、robots.txt、RFC 9309、外贸独立站

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。