蜘蛛IP验证过了还是假爬虫?白名单实战

用反向 DNS 验证来访者是不是真百度蜘蛛,是站长圈用得非常多的做法。但这套方法有几个失效场景:PTR 记录可以自己配、云主机的 IP 和官方抓取段可能同属一个 ASN、采集服务甚至能直接租用蜘蛛来源。本文用 9 个问答讲清蜘蛛验证为什么会失效、三段式验证怎么写、官方 IP 段怎么精确收窄,以及怎么在不误伤真蜘蛛的前提下拦掉伪装流量。

Q:常见的蜘蛛验证方法有哪几种?各自能挡住什么?

按强度从低到高排,越往后越难绕过,但成本也越高。

  • 只看 User-Agent(最弱)。一个字符串而已,curl -A 就能改。只能过滤掉最懒惰的扫描器,对真正的伪装有防御价值约等于零。
  • 反向 DNS 正查 + 反查(中等)。先 dig -x 拿到 PTR 域名,再正查该域名确认解析回来的是同一 IP。这一步能挡住大量伪造 UA 的脚本。
  • 官方 IP 段精确比对(较强)。把来源 IP 和搜索引擎官方公布的抓取段做精确比对,只放行名单内的地址。
  • 行为特征判定(最强,也是最后一道)。看请求频率、路径分布、是否遵守 robots.txt、是否抓 POST 与接口。

结论很直接:单靠任何一种都能被绕过,必须叠加使用。而多数站点的实际配置是第一种或第二种单独存在,这就是漏子的来源。

Q:反向 DNS 验证为什么会被绕过?

三类情况,每一类都很现实。

第一类:PTR 记录允许自定义,反查结果本身就可以伪装。

不少 IDC 和云主机允许用户自行设置 PTR 记录。攻击者把 PTR 设成带 spider、baidu、google 字样的域名,你单纯做一次 dig -x 看到域名里含 spider 就放行,等于没验证。所以只看 PTR 字符串是不够的,必须做正查回验——把 PTR 域名解析回来,确认结果里包含请求来源那个 IP。

# 反查
dig -x 180.76.15.x +short
# 正查回验(把上一步结果解析回来,必须包含原 IP)
dig +short ir1.baidu.com

第二类:官方抓取段和云租户段同属一个 ASN。

这是最隐蔽也最常见的一种。搜索引擎的抓取服务器本身就跑在自己的云上,于是官方抓取 IP 和普通云主机租户的 IP 属于同一个 ASN、甚至相邻网段。运维为了省事按 ASN 放行,结果整朵云的租户都被一并放行了。所以放行粒度必须是精确前缀,绝不能是 ASN。

第三类:采集服务直接租蜘蛛来源。

市面上存在商业采集服务,专门提供「已通过验证的蜘蛛来源」,本质是在官方段内或相邻段租机器发起请求,UA 也照抄。这种流量 IP 反查能过、UA 也对,唯一暴露它的是行为——抓取频率、路径覆盖、并发数全都不符合官方蜘蛛的习惯

Q:官方蜘蛛 IP 段怎么拿、怎么精确收窄?

优先用官方提供的机器可读清单,别用论坛里抄来的静态列表。

# Google 官方蜘蛛段(机器可读,含前缀长度字段)
curl -s https://developers.google.com/static/search/apis/ipranges/googlebot.json

# Bing 官方蜘蛛段
curl -s https://www.bing.com/toolbox/bingbot.json

百度官方没有提供 JSON 接口,需要从百度搜索资源平台公布的蜘蛛 IP 段页面定期同步,建议写成脚本每季度拉一次人工核对,不要指望一份永不更新的名单。

拿到段之后转成 Nginx 可用的 geo 白名单:

# /etc/nginx/conf.d/spider_whitelist.conf
geo $is_official_spider {
    default 0;
    # 百度蜘蛛段(示例,务必以官方最新公布为准)
    180.76.15.0/24   1;
    220.181.108.0/24 1;
    # Googlebot
    66.249.64.0/19   1;
}

两个必须遵守的纪律:

  • 只放行精确前缀,不要放行整个 ASN。前面说过,ASN 放行等于把整朵云放进来。
  • 名单要有更新机制。运营商段会调整,官方新增节点也不会通知你。建议在监控里加一条:白名单来源的请求占比突然下降就提示核查名单是否过期。

Q:验证代码怎么写才算严谨?

标准做法是四步串行,任何一步不过就判定为非官方蜘蛛。

import socket, ipaddress

OFFICIAL_PREFIXES = [ipaddress.ip_network(x) for x in [
    "180.76.15.0/24", "220.181.108.0/24", "66.249.64.0/19",
]]

def verify_spider(client_ip, ua):
    # 步骤 1:UA 粗筛(只做粗筛,不作为依据)
    if not any(k in ua for k in ("Baiduspider", "Googlebot", "bingbot")):
        return False

    # 步骤 4 前置:先做白名单精确比对,最省资源
    ip = ipaddress.ip_address(client_ip)
    if not any(ip in n for n in OFFICIAL_PREFIXES):
        return False

    # 步骤 2:反查 PTR
    try:
        ptr, _, _ = socket.gethostbyaddr(client_ip)
    except Exception:
        return False

    # 步骤 3:正查回验,结果必须包含原 IP
    try:
        forward = {r[4][0] for r in socket.getaddrinfo(ptr, None)}
    except Exception:
        return False
    return client_ip in forward

三个实现细节决定成败:

  • 先做白名单比对,再做 DNS 查询。DNS 查询是有网络开销的慢操作,把最便宜的判断放最前面,能省下大量无意义的查询。
  • DNS 结果必须缓存,但 TTL 要短。每次请求都做两次 DNS 查询,攻击者用海量请求就能让你的 DNS 解析成为瓶颈——反而被打挂。缓存 5 到 10 分钟足够,同时设置最大缓存条目数防止内存被撑爆。
  • 验证失败要有快速路径。反查超时要有严格超时时间(比如 1 秒),不能让 DNS 卡住整个请求链路。反查失败就直接按非蜘蛛处理,不要重试。

Q:过了 IP 验证就一定安全吗?还要看哪些行为特征?

不一定。这是本文最需要修正的认知:IP 验证通过只说明来源地址在官方段内,不说明这次抓取就是官方行为

真实官方蜘蛛的行为画像相当稳定:

  • 严格遵守 robots.txt 里的抓取间隔;请求间隔平稳(通常 1 到 3 秒一个),不会出现突发并发。
  • 只抓 HTML 页面和静态资源,基本不会去抓 API 接口、不会发 POST 请求
  • 不会访问 /admin、/login 这类管理路径,也不会抓带明显非页面参数的 URL。
  • 请求头里带 If-Modified-Since、If-None-Match 做增量抓取,UA 完整无残缺。

而滥用流量的特征几乎是反面:

  • 在官方段内以远超正常的频率抓取,尤其集中打搜索页、列表页做参数遍历。
  • 抓接口、抓带 token 的链接、发 POST。
  • 访问后台路径和不存在的高熵 URL(配合漏洞扫描)。

用日志快速验证官方段内有没有超速行为:

# 假设 spider_ips.txt 是官方段内出过现的 IP 清单,统计每个 IP 的请求量
grep -Ff spider_ips.txt /var/log/nginx/access.log | awk '{print $1}' \
  | sort | uniq -c | sort -rn | head -20

# 单个 IP 的每分钟请求峰值(找突发并发)
grep "220.181.108.5" /var/log/nginx/access.log \
  | awk '{print substr($4,2,17)}' | uniq -c | sort -rn | head

判定逻辑很清楚:官方段内 + 请求量正常 = 真蜘蛛;官方段内 + 突发高频或抓接口 = 被滥用的来源,需要限速

Q:白名单放行之后还要限速吗?配置怎么写?

要,而且这是整套方案里最关键的设计原则:白名单只免除「拦截」,不免除「限速」

很多站点的白名单是一刀切放行——名单内直接跳过全部检查,这等于把验证通过后的流量变成无人监管的通道。正确做法是让白名单来源仍然走限速,只是阈值比普通爬虫宽松得多。

# 官方蜘蛛白名单命中时拿到限速键(其余请求得到空字符串,不计入限速)
map $is_official_spider $spider_limit_key {
    default "";                  # 非白名单:不占用这个 zone
    1       $binary_remote_addr;
}

# 对白名单来源做温和限速:2 r/s 不影响正常收录,但能压住被滥用的高频抓取
limit_req_zone $spider_limit_key zone=spider:10m rate=2r/s;
limit_req_status 429;

server {
    location / {
        limit_req zone=spider burst=10 nodelay;
    }
}

这里用的是 map 空键技巧:Nginx 对限速键为空字符串的请求不做计数,所以只有白名单来源会被这个 zone 限制,普通用户和普通爬虫完全不受影响。这样一来:

  • 真蜘蛛照常抓取,2 r/s 对收录节奏毫无影响。
  • 租用官方段搞高频采集的来源会被自动限速到 2 r/s,抓同样量级的数据要多花几十倍时间。
  • 不需要人为判断谁是「好蜘蛛坏蜘蛛」,规则自己生效。

改完务必 nginx -t 校验再 reload。另外那个 nodelay 不要去掉,去掉后 burst 队列会排队导致响应变慢,对蜘蛛不友好。

Q:怎么在不误伤真蜘蛛的前提下拦掉伪装请求?

答案是三档处置,而不是非黑即白的放行与封禁。

  • 第一档:验证通过 + 行为正常 → 正常放行。这是绝大多数真蜘蛛的状态。
  • 第二档:验证通过 + 行为异常(超速、抓接口、访问后台)→ 限速 + 告警,不要直接封。因为误封官方蜘蛛的代价很高,而限速已经能解决资源消耗问题。
  • 第三档:验证失败 → 按普通爬虫规则处理,走常规限速与 UA 黑名单,该封就封。

误伤真蜘蛛的代价必须清楚:收录量下降、新页面不被抓、长尾流量慢慢掉光,而且很难第一时间发现。所以对白名单来源建议单独打一份日志(Nginx 里用 map 变量配 access_log),便于观察收录请求量的变化趋势。

告警要设在「变化」上而不是「绝对值」上:白名单来源的请求量相比前 7 天均值突变(尤其暴涨 10 倍以上)就要通知。暴涨有两种可能,要么真有新内容被集中抓取,要么是有人在租用蜘蛛来源刷你的站,两种情况都值得看一眼。

Q:日志里怎么快速分辨真假蜘蛛?

两条命令就能把「声称是蜘蛛」和「真的是蜘蛛」分开。

先把所有自称蜘蛛的来源 IP 提出来,再和官方段做差集:

# 提取所有自称百度蜘蛛的请求来源 IP
grep -i "baiduspider" /var/log/nginx/access.log \
  | awk '{print $1}' | sort -u > claimed_spider.txt

# 提取官方段清单(每行一个 IP 或 CIDR)
sort -u official_spider_cidr.txt > official.txt

# 用 CIDR 精确比对(注意 comm 只能做字符串比对,用脚本更准确)
python3 - <<'PY'
import ipaddress
nets = [ipaddress.ip_network(l.strip()) for l in open("official.txt") if l.strip()]
for l in open("claimed_spider.txt"):
    ip = l.strip()
    if not ip: continue
    a = ipaddress.ip_address(ip)
    if not any(a in n for n in nets):
        print(ip)          # 声称是蜘蛛但不在官方段 = 假蜘蛛
PY

输出不在官方段的这些地址,大概率是伪造 UA 的脚本,直接进 UA 黑名单或按 IP 段限速。然后再用第一条命令统计官方段内每个 IP 的请求量,找出「验证通过但明显超速」的那批,走限速而非封禁。

建议把这两条检查做成每日定时任务,结果自动写入日报。蜘蛛验证不是一次性配置,而是持续运维——名单会过期,伪装手法会升级,只有日志在持续说话。

Q:还有什么补充建议?

把整套动作清单化,接站或排查时逐条过:① UA 粗筛 → ② 反查 PTR + 正查回验(含 DNS 缓存与超时)→ ③ 官方 IP 段精确白名单,季度更新 → ④ 白名单只免拦截不免限速 → ⑤ 白名单来源单独日志观察收录量 → ⑥ 官方段内超速与抓接口行为告警 → ⑦ 声称是蜘蛛但不在官方段的 IP 进黑名单

回到本质:蜘蛛验证的目标不是「认出谁是蜘蛛」,而是「把冒充蜘蛛的成本抬到不划算」。只看 UA 时,伪装成本是改一个字符串;加上正查回验,成本变成要控制一台能配 PTR 的机器;再加上精确段比对和行为限速,成本就变成必须在官方段内租机器、并且还得忍受 2 r/s 的速度——到这一步,绝大多数采集方会主动放弃,转去挑更容易的目标。

所以不要追求某一招「绝对可靠」。IP 白名单负责准确性,行为限速负责兜底,日志监控负责发现变化,三层叠起来才能既保住 SEO 收录,又不给伪装流量留后门。