如何排查网站是否被“小偷程序”采集:从访问日志入手

教程3天前发布 jdkjadmin
0 00
如何排查网站是否被“小偷程序”采集:从访问日志入手

如何排查网站是否被“小偷程序”采集:从访问日志入手

网站内容突然出现在别处,或者服务器流量、带宽和数据库负载异常时,访问日志通常是最先值得检查的证据。它能帮助回答一些具体问题:哪些客户端在什么时间请求了哪些页面、请求频率有多高、是否集中访问文章或图片、是否反复触发分页和搜索接口。

不过,日志不能直接告诉你“这个 IP 一定是小偷程序”。搜索引擎、监控服务、浏览器预加载、企业出口代理和普通用户,都可能呈现出自动化访问的特征;反过来,采集程序也可以更换 IP、放慢速度或伪装 User-Agent。比较可靠的做法是先从日志中找出异常模式,再结合内容相似度、时间线、请求资源和来源信息交叉验证。

一、适用场景与排查边界

本文适用于能够访问 Web 服务器访问日志的网站运维人员,示例以常见的 Nginx combined 格式为主。Apache 使用相近格式时,思路可以复用,但日志字段和配置需要按实际情况调整。如果网站位于 CDN、负载均衡器或反向代理之后,还要先确认日志记录的是哪一层的客户端地址。

访问日志通常能提供以下线索:

  • 某个来源在短时间内请求了大量文章、商品或图片页面。
  • 请求路径呈现规律性,例如遍历连续编号、分页或分类目录。
  • 同一 User-Agent 对大量页面进行高速访问,且请求间隔稳定。
  • 某些页面被持续重复请求,或采集后出现异常的 404、403、429 状态码。
  • 请求时间与外站转载内容首次出现的时间大致对应。

日志的边界同样重要:

  • IP 地址不是身份。 多个用户可能共享一个 NAT 出口;采集者也可能使用代理、云主机或不断变化的地址。
  • User-Agent 可以伪造。 声称自己是搜索引擎,并不能证明请求来自该搜索引擎。
  • 请求不等于成功获取内容。 访问日志记录的是服务器处理的 HTTP 请求,不一定代表完整响应已被客户端收到或保存。
  • 请求量不等于采集量。 缓存、条件请求、页面中的静态资源、客户端重试都会影响计数。
  • 日志保留范围有限。 轮转、采样、CDN 缓存和上游日志配置都可能造成缺失。

因此,排查结论宜写成“某来源的访问模式与自动化采集相符”,而不是仅凭一次统计就断言“已确认盗取”。

二、先确认拿到的是哪一份日志

Nginx 常见访问日志位于 /var/log/nginx/access.log,但实际路径取决于站点配置。先查看配置中的 access_log 和 log_format:

sudo nginx -T 2>/dev/null | grep -nE 'log_format|access_log'

常见的 combined 日志行大致如下:

203.0.113.24 - - [15/May/2025:10:20:30 +0000] "GET /news/example HTTP/1.1" 200 18432 "https://example.com/" "Mozilla/5.0 ..."

其中通常包括客户端地址、时间、请求行、状态码、响应字节数、Referer 和 User-Agent。不同站点可能采用自定义格式,例如将真实客户端地址写入额外字段,或完全不记录 Referer。分析前应确认字段含义,不能把自定义日志误当成 combined 格式处理。

反向代理与 CDN 特别注意

如果 Nginx 前面还有 CDN 或负载均衡器,默认的 $remote_addr 可能是代理服务器地址,而不是访客地址。此时要查明:源站日志记录了哪一个字段、该字段是否由可信代理写入,以及代理是否会覆盖客户端提交的同名请求头。

不能直接相信任意请求中的 X-Forwarded-For。如果源站允许外部用户直接连接,攻击者可以自行伪造这个头,导致日志里的“客户端 IP”不可信。只有在网络层限制源站仅接受可信代理流量,并按代理产品文档配置真实地址解析后,相关字段才适合用于归因。若 CDN 提供独立访问日志,通常应把 CDN 日志与源站日志结合查看。

三、先用命令行快速找出异常来源

对标准 combined 格式,可先统计请求最多的客户端地址:

LOG=/var/log/nginx/access.log
awk '{print $1}' "$LOG" | sort | uniq -c | sort -nr | head -20

这只是一个粗略排行榜。它把日志第一列当作客户端地址,因此不适用于第一列不是 IP 的自定义格式;多个用户共用一个出口时,也可能被合并统计。

再看状态码分布,判断高请求量是否伴随大量失败:

awk -F '"' '{print $3}' "$LOG" \
  | awk '{print $1}' \
  | sort | uniq -c | sort -nr

这个写法依赖请求行和状态码位于双引号后的常见格式。遇到带有特殊字段、日志格式不同或引号处理不一致的记录时,结果可能不准确。它适合快速筛查,不适合用作审计结论。

查看某个候选 IP 请求最多的路径,可以使用:

IP=203.0.113.24
grep -F "$IP " "$LOG" | awk -F '"' '{print $2}' \
  | awk '{print $2}' | sort | uniq -c | sort -nr | head -30

grep -F 会按普通文本匹配。对于共享网段、相似字符串或自定义格式,应使用能正确解析字段的工具,而不是依赖简单文本匹配。日志文件也可能包含敏感 URL 参数;把输出贴到公共论坛或第三方在线工具之前,应先脱敏。

日志较大时,不建议直接把整份文件反复复制到个人电脑。可以在服务器上筛选时间范围、压缩归档文件,或使用日志平台的聚合查询;同时检查磁盘余量,避免分析过程额外占满存储空间。

四、用 Python 汇总 IP、路径和 User-Agent

下面的脚本使用 Python 3 标准库,适用于每行基本符合 Nginx/Apache combined 格式的日志。它统计每个 IP 的总请求数、不同路径数、状态码分布、热门路径和 User-Agent。URL 查询参数会被去掉,避免将不同参数值展开显示;脚本不会主动访问网络,也不会修改日志。

将代码保存为 analyze_access.py:

#!/usr/bin/env python3
import argparse
import re
from collections import Counter, defaultdict
from urllib.parse import urlsplit

# 适用于常见 combined 格式:
# IP ident user [time] "METHOD target protocol" status bytes "referer" "UA"
LINE_RE = re.compile(
    r'^(?P<ip>\S+) \S+ \S+ '
    r'\[(?P<time>[^\]]+)\] '
    r'"(?P<method>\S+) (?P<target>.*?) (?P<proto>[^"]+)" '
    r'(?P<status>\d{3}) (?P<bytes>\S+) '
    r'"(?P<referer>[^"]*)" "(?P<ua>[^"]*)"'
)

def safe_path(target):
    # 只统计路径,不显示查询参数。
    # urlsplit 对以 / 开头的普通请求目标可直接使用。
    path = urlsplit(target).path or "/"
    return path[:300]

def main():
    parser = argparse.ArgumentParser(
        description="汇总 combined 格式的 Web 访问日志"
    )
    parser.add_argument("logfile", help="日志文件路径;使用 - 表示标准输入")
    parser.add_argument("--top", type=int, default=20,
                        help="输出多少个 IP,默认 20")
    parser.add_argument("--min-requests", type=int, default=1,
                        help="只显示达到该请求数的 IP,默认 1")
    args = parser.parse_args()

    if args.top < 1 or args.min_requests < 1:
        parser.error("--top 和 --min-requests 必须大于 0")

    stats = defaultdict(lambda: {
        "requests": 0,
        "paths": Counter(),
        "statuses": Counter(),
        "uas": Counter(),
    })
    parsed = 0
    skipped = 0

    if args.logfile == "-":
        import sys
        stream = sys.stdin
        close_stream = False
    else:
        stream = open(args.logfile, "r", encoding="utf-8", errors="replace")
        close_stream = True

    try:
        for line in stream:
            match = LINE_RE.match(line.rstrip("\n"))
            if not match:
                skipped += 1
                continue

            row = match.groupdict()
            item = stats[row["ip"]]
            item["requests"] += 1
            item["paths"][safe_path(row["target"])] += 1
            item["statuses"][row["status"]] += 1
            # 避免终端输出过长的 UA 字符串
            item["uas"][row["ua"][:180]] += 1
            parsed += 1
    finally:
        if close_stream:
            stream.close()

    ranked = sorted(
        (
            (ip, data) for ip, data in stats.items()
            if data["requests"] >= args.min_requests
        ),
        key=lambda pair: pair[1]["requests"],
        reverse=True,
    )[:args.top]

    print(f"成功解析 {parsed} 行;未匹配格式 {skipped} 行;独立 IP {len(stats)} 个")
    for ip, data in ranked:
        print(f"\nIP: {ip}  请求: {data['requests']}  不同路径: {len(data['paths'])}")
        print("状态码:", ", ".join(
            f"{code}={count}"
            for code, count in data["statuses"].most_common()
        ))
        print("常见路径:")
        for path, count in data["paths"].most_common(10):
            print(f"  {count:6d}  {path}")
        print("常见 User-Agent:")
        for ua, count in data["uas"].most_common(5):
            print(f"  {count:6d}  {ua or '(空)'}")

if __name__ == "__main__":
    main()

运行示例:

python3 analyze_access.py /var/log/nginx/access.log --top 30 --min-requests 100

若要分析压缩的轮转日志,可通过管道输入:

zcat /var/log/nginx/access.log.2.gz | python3 analyze_access.py - --top 30

脚本对无法匹配的行会计入“未匹配格式”,不会悄悄把它们当成有效请求。若未匹配数量很高,应先核对日志格式,而不是修改正则强行接受所有输入。脚本按 IP 汇总,无法自动识别 NAT、代理链或真实访问者;它也没有按时间窗口计算速率,因此不能单独判断“每秒多少次”。分析大量日志时,可先按日期拆分,或改用适合流式统计和时间窗口聚合的日志平台。

五、怎样判断“像采集”,而不是普通高流量

可疑程度通常来自多个迹象的组合,而不是某个单一阈值。对候选来源,建议至少检查以下方面:

1. 请求的内容类型和路径分布

逐条观察它访问的是文章正文、商品详情、分类页、图片,还是登录、搜索、接口和静态资源。一个客户端请求页面中的 CSS、JS 和图片,可能是正常浏览器渲染;只请求正文页面也可能是采集器,但并非必然。要留意页面链接结构:按栏目遍历、连续翻页、编号递增、短时间抓取大量历史内容,比单纯请求量高更有解释力。

2. 请求节奏与时间跨度

查看请求是否集中在短时间内,间隔是否近乎固定,是否全天持续,是否在短暂停顿后重复扫过同一批 URL。服务器访问日志通常记录请求时间,但聚合统计可能掩盖瞬时突发;最好按分钟甚至更细粒度观察。不同网站的合理访问量差异很大,不存在适用于所有站点的“每分钟超过多少次就一定是采集”的通用门槛。

3. 状态码、响应大小和重复行为

连续访问大量页面且大多返回 200,说明服务端成功处理了不少请求,但不能证明对方保存了响应内容。大量 304 表示条件请求命中缓存协商,并不等于对方没有已有副本;大量 404 可能是错误链接、扫描或采集器猜路径,也可能来自失效的站内链接。响应字节数可以作为线索,但日志里记录的具体字段可能是发送字节、响应体字节或自定义值,要先看 Nginx 日志变量定义。

4. User-Agent 和来源信息

空 User-Agent、明显的脚本库标识或自称爬虫的字符串,都可以用于筛查,但都可伪造。对声称来自某个搜索引擎的请求,不要只看名称;应按该服务公开的官方验证流程核验域名或 IP。Referer 也不是强身份凭证,客户端可以不发送或自行填写。

5. 与外部转载内容交叉验证

如果在其他网站发现高度相似的文章,可记录页面地址、首次发现时间、内容差异及截图或存档等证据,再与访问日志的请求时间、路径和来源模式对照。要注意外站页面的发布时间可能被回填或改写;某 IP 请求某篇文章,也不代表该 IP 所属主体就是转载者。技术线索和法律归属不是同一件事。

六、为后续分析补齐日志字段

如果现有日志不足以分析,可在变更前评估磁盘容量、隐私和性能,再增加必要字段。下面是一个 Nginx 示例,记录请求时间、地址、请求目标、状态码、发送字节数、处理耗时和 User-Agent:

http {
    log_format audit
        '$remote_addr [$time_local] "$request" '
        '$status $body_bytes_sent $request_time '
        '"$http_referer" "$http_user_agent"';

    access_log /var/log/nginx/access_audit.log audit;
}

配置修改后先检查语法,再平滑重载:

sudo nginx -t
sudo systemctl reload nginx

实际部署时,应把配置放在适当的 http、server 或 location 上下文,并确认没有其他配置覆盖预期日志。加入 $request 会记录请求目标,查询参数中可能含有邮箱、令牌或其他敏感信息;若应用会在 URL 中放置秘密值,不应未经评估就把原始请求目标长期写入日志。必要时使用不记录查询参数的格式或在日志平台侧做脱敏。

日志的保存期限也应符合业务、隐私和合规要求。日志可能包含 IP 地址、访问路径、账号标识或其他个人信息,应限制读取权限、制定保留周期,并避免把原始日志随意传到外部服务。访问控制和日志轮转是分析方案的一部分,不是事后补救。

七、常见错误与误报来源

  • 仅凭单个 IP 的请求数定性。 搜索引擎抓取、健康检查和大型公司出口可能产生高请求量。应结合路径、节奏、状态码和官方身份验证。
  • 把 User-Agent 当作认证信息。 字符串很容易伪造。允许或阻挡请求时,不要只靠 User-Agent 名称做安全边界。
  • 把共享出口中的所有请求归为一个主体。 NAT、校园网、移动运营商出口和代理服务会聚合许多真实用户。对单个 IP 限流可能误伤整个出口。
  • 把 IP 归属查询当成责任证明。 IP 注册机构、云厂商和地理位置数据库只能提供有限线索,地址可能动态分配或由代理使用。
  • 忽略 CDN 缓存造成的视角差异。 命中 CDN 的请求可能不会到源站;源站日志没有记录,不代表用户没有访问过。反之,回源请求的来源也可能是 CDN 节点。
  • 混淆查询参数与路径。 同一条路径带不同参数可能是重复抓取,也可能是正常筛选、分页或追踪参数。分析时应分别观察规范化路径和参数含义,不要在未经审查时把参数值公开。
  • 直接在生产环境运行耗资源的全量分析。 大文件排序会使用 CPU 和临时磁盘空间。先估算文件大小,尽量在副本、离峰时段或专用日志平台处理。
  • 依据一次短时异常就长期封禁。 临时抓取、发布活动或爬虫集中重试都可能形成尖峰。先复核时间范围,再选择短期、可回滚的措施。

八、发现疑似采集之后怎么处理

先保留原始证据。记录日志文件来源、时区、查询时间范围、哈希值或只读副本,并注明分析脚本版本和筛选条件。保留证据时也要遵循组织的数据访问和隐私规范,避免把未经脱敏的用户信息扩大传播。

然后按风险分层处理:

  1. 低影响、尚未确认。 继续观察,增加适度的按路径和时间窗口统计,核对内容转载情况,不急于封禁。
  2. 明显异常且影响资源。 使用 CDN、WAF、反向代理或应用层做有期限的限速、挑战或针对性规则。设置监控和回滚方式,并关注误伤正常用户的投诉、错误率与转化数据。
  3. 持续高负载或影响可用性。 优先保护服务稳定性,例如对昂贵接口设置合理速率限制、使用缓存、限制不必要的批量查询,并按事件流程升级处理。不要把对单个 IP 的阻断当成唯一防护。
  4. 涉及版权或合同争议。 保存可核验材料,检查站点条款、授权范围和适用法律,必要时咨询专业人士。访问日志本身通常不足以单独证明侵权主体及其行为。

Robots Exclusion Protocol(常见的 robots.txt)用于告知遵循规则的爬虫哪些路径不希望被抓取。它不是访问控制机制,也不能阻止不遵守规则的程序。真正需要保密或限制访问的内容,应采用身份认证、授权校验、网络访问控制等措施,而不是只依赖 robots.txt、隐藏链接或修改页面地址。

同样,封禁 IP、修改 User-Agent 规则或对页面做混淆,都不能可靠阻止有能力更换来源的采集者。若内容不应公开,应从访问权限和数据发布流程解决;若内容本来公开,防护重点通常是控制资源消耗、提高可追溯性,并按实际情况处理转载问题。

九、建议的排查顺序

一个可复用的流程可以压缩为六步:

  1. 确定日志在哪一层产生、客户端 IP 字段是否可信、时区和日志格式是什么。
  2. 按时间范围统计来源请求量,找出偏离本站正常基线的候选对象。
  3. 查看候选对象的路径、时间分布、状态码、响应大小和 User-Agent。
  4. 结合 CDN/WAF 日志、服务器负载曲线和外部转载页面交叉验证。
  5. 保存必要证据,区分“可疑模式”和“已确认行为”,记录不确定性。
  6. 按影响程度采取限速、监控、访问控制或法律咨询等措施,并观察误伤与效果。

访问日志最适合回答“发生了什么请求”,不适合单独回答“请求者是谁、是否保存了内容、是否构成侵权”。把日志当作一组可核验的线索,先校准数据来源,再做多维度比对,才能把误报控制在合理范围内,并让后续的技术和业务处置有可靠依据。

© 版权声明

相关文章