
这两天服务器一直不太平。凌晨两点我习惯性打开top看一眼nginx 进程的数字飙得吓人——平时占用不到 10% 的 CPU直接顶到了 90% 以上。再去看 nginx 的 access.log短短一个多小时已经刷了三十多万行。我第一反应是被人刷了第二反应是赶紧查来源。结果一看傻眼了同一个逻辑地址段靠着一套固定的 User-Agent正在疯狂翻我的文章列表页和详情页。这就是典型的“网站小偷”——不是黑客入侵那种高门槛攻击而是直接用脚本批量抓取站点内容搬到别的平台去。以前遇到这种事我得自己钻日志、写规则、反复封 IP少说也要折腾大半天。这回不一样我全程让 OpenAI 的 Codex CLI终端里的 AI 编程助手帮我分析、出方案、写脚本前后两个多小时就完成了从发现到阻断的全流程。今天这篇就是一次完整复盘把每个能落地的细节都摊开讲。如果你是自己运维博客、内容站点或者在公司管着几台业务服务器这篇文章可以直接照着抄。我会把怎么定位异常、怎么让 Codex 帮你干活、怎么写三层防护、以及过程中踩到的各种坑都按时间线拆开。废话不多说开始。1. 先搞清楚“小偷”是谁异常流量识别与溯源1.1 从一条异常日志说起很多人看到服务器负载变高第一反应是“加配置”“上 CDN”但如果不先弄清流量长什么样加多少资源都白搭。这次我从 nginx 的 access.log 里抽了几行特征非常明显192.168.10.5 - - [15/Mar/2025:02:17:41 0800] GET /article/1024 HTTP/1.1 200 10240 - python-requests/2.31.0 192.168.10.5 - - [15/Mar/2025:02:17:42 0800] GET /article/1025 HTTP/1.1 200 10240 - python-requests/2.31.0 192.168.10.6 - - [15/Mar/2025:02:17:44 0800] GET /list/3 HTTP/1.1 200 10240 - python-requests/2.31.0两秒钟之内连续抓取相邻 ID 的文章没有 Referer没有 CookieUA 清一色是python-requests。这就是典型的批量遍历。更隐蔽的在于它不抓任何图片、CSS、JS只抓 HTML 正文。换句话说对方要的就是文章内容本身连“伪装成正常用户”的功夫都不想做。我当时的判断只有一条内容站被盯上了。对方可能是做垃圾站、AI 洗稿素材库、或者干脆是同行的采集程序。这种事情放在十年前要靠人工看日志现在有了 AI 编程工具第一步就可以让它介入。1.2 用 Codex 做第一轮日志“快筛”我踩过不少“把日志直接扔给 AI”的坑。几十万行原始日志扔给任何大模型都不现实不仅上下文放不下就算塞进去大模型面对海量重复行也会失去重点。正确的做法是先用 Linux 命令把日志聚合成“小表格”再交给 Codex 判断。第一步我跑了一条聚合命令统计 Top 30 的访问 IPawk {print $1} /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -30输出很快就给出了明确答案。排在前面的几个 IP 贡献了当天 60% 以上的请求。我把这段输出复制给 Codex指令写得尽量具体你是我的运维搭档。这是我的 nginx access.log 按 IP 聚合后的 Top 30 结果。 请帮我判断哪些 IP 像恶意采集者给出可疑度评分并告诉我下一步该查什么。Codex 的回复没有让我失望。它指出前五个 IP 请求量异常集中且相邻 IP 同属一个连续地址段大概率是同一批人控制的。它建议我继续统计“这些 IP 的 User-Agent 分布”和“请求 URL 的路径分布”用来确认是人工踩点还是纯脚本遍历。我顺着它给的命令继续awk {print $1, $6} /var/log/nginx/access.log | grep -E ^192\.168\.10\. | sort | uniq -c | sort -nr | head -20到这里画像已经很完整了固定 UA连续 URL长时间无静态资源请求。整个过程我只花了不到十分钟就从一个“服务器很卡”的现象收敛到“某几个 IP 在批量抓内容”的结论。1.3 判定恶意采集的三条经验标准如果你也想自己判断“这是不是小偷”不需要多高深的技巧认准三条经验标准就行。第一没有浏览器指纹。正常用户访问一定会有像样的 UAChrome、Edge、Firefox 等会带 Referer会有 Cookie恶意脚本最省事的写法就是直接用一个 HTTP 库的默认 UA。第二请求序列机械且无随机延迟。文章 ID 一条条递增、翻页页数按 1、2、3、4 走、访问时间间隔几乎恒定人类不会这么干。第三不加载静态资源。我从日志里单独统计了图片和 CSS 的数量awk {print $7} /var/log/nginx/access.log | grep -E \.(jpg|css|js|png) | wc -l正常用户访问内容站静态资源请求占比通常在 60% 以上而恶意采集者的请求里这一项几乎趋近于零。因为脚本只需要 HTML 正文不需要浏览器渲染。这三条标准放在一起就可以构成一个非常可靠的“异常评分”同时满足两条以上基本可以断定是自动程序。别急着封先把证据留下来后面写防护规则时都用得上。2. Codex 环境搭建与接入准备2.1 安装与登录先扫平“auth token is unavailable”这类小坑这篇文章不是 Codex 的安装手册但我必须把实际使用中遇到的两个坑先提一下因为它们会卡住新手很久。官方安装方式很简单如果你的机器有 Node.js 环境直接npm install -g openai/codex codex --version装好之后需要登录。运行codex login它会打开浏览器引导你完成授权。如果这一步报codex auth token is unavailable不要慌十有八九是 token 没写进配置文件或者已经过期。先重新执行一次codex login再检查本地配置文件是否存在cat ~/.codex/auth.json只要这个文件里有有效的 tokenCLI 就能正常工作。我自己遇到过的情况是系统装了多个 Node 版本codex命令被全局安装到了另一个路径导致读不到正确的 auth 文件。解决方式也很简单直接用which codex看清楚可执行文件位置再统一环境变量。2.2 把网站上下文交给 Codex目录与配置的准备工作Codex 不是算命先生它不知道你的网站是什么架构、日志在哪、nginx 规则长什么样。想让它的建议能落地第一步就是“喂上下文”。我专门在服务器上建了一个临时工作目录把相关文件都归拢在一起mkdir -p ~/antibot/context cp -r /etc/nginx/conf.d ~/antibot/context/ cp /var/log/nginx/access.log ~/antibot/context/sample.log然后在终端里用 Codex 对话模式codex进去后可以像聊天一样提问给它一段非常明确的“背景说明”你是我的运维搭档。目标网站是 Nginx PHP 的内容站点。 日志路径在 /var/log/nginx/我目前已经把 nginx 配置和日志样本放到了 ~/antibot/context/。 我们要拦截的是恶意内容采集者但不能影响搜索引擎爬虫和正常读者访问。 请先给我一份“诊断清单”告诉我需要收集哪些数据、确认哪些风险。这一步的价值不是让 Codex 马上写脚本——它还不够了解环境。让它先输出“你还需要什么”往往比直接让它干活更高效。它给我列了四件事确认日志格式、统计 UA 分布、查看现有 nginx 限流配置、确认站点是否套了 CDN。都是合理问题我逐一回答后后面的方案就明显贴合我的实际情况了。2.3 接第三方模型与两个常见报错处理Codex 默认使用官方服务但很多人会因为接口权限、成本或者部署环境原因想接兼容 OpenAI 接口的第三方模型。Codex 是支持在codex.json里配置模型提供方的。我在一台备用服务器上做过一次接入测试配置大致长这样{ model: deepseek-chat, model_provider: deepseek, providers: { deepseek: { base_url: https://api.deepseek.com/v1, env_key: DEEPSEEK_API_KEY } } }把DEEPSEEK_API_KEY配到环境变量里Codex 就能走这个兼容接口。这样做的意义在于如果你的团队已经有统一的大模型网关可以让 Codex 和公司现有模型体系打通不额外引入新服务。配置模型时有个报错必须留意。网上很多人贴过类似报错我自己也不小心踩过the gpt-5.6-sol model is not supported when using codex with a ...这通常是因为codex.json里的model字段写了一个当前版本不支持的模型名。解决方式有两种要么换成官方支持的模型名要么只写模型别名让 Codex 自己映射。别去硬核检查这个错误的具体代码逻辑先检查模型名拼写成功率最高。另一个常见报错也很典型cc switch local proxy failed while handling codex endpoint /responses我看到这个报错的第一反应是“本地转发服务出问题了”。这通常是机器上开了抓包、调试转发工具或者系统环境变量里残留了 HTTP(S) 转发配置导致 Codex 的请求走了异常出口。处理方式很固定先关掉抓包调试工具再清理环境变量里http_proxy和https_proxy这两个值然后重启终端重新运行codex。这个坑最大的特点是提示信息很吓人但实际就是个“网络通道走错口”的小问题。3. 三层防护的落地过程3.1 第一层Nginx 限流与 UA/IP 拦截5 分钟先止血方案设计之前我的目标很明确先止血再持久防御。所谓“止血”就是让恶意流量立刻不再压垮服务器。第一道防线直接落在 Nginx 上两个动作按 User-Agent 拦截已知异常工具按 IP 速率限制请求频率。UA 拦截我推荐用map而不是if里写正则可读性和维护性都好很多。在nginx.conf的http块里加map $http_user_agent $is_bad_ua { default 0; ~*python-requests 1; ~*curl/ 1; ~*Go-http-client 1; ~*Apache-HttpClient 1; ~*scrapy 1; }然后在server块里server { listen 80; server_name yourdomain.com; if ($is_bad_ua 1) { return 403; } }光拦 UA 还不够因为对方随时可以换个 UA 继续打。所以还要叠加限流。在http块里定义limit_req_zone $binary_remote_addr zoneanti_bot:20m rate10r/m;然后在需要保护的 location 里启用location ~ ^/(article|post|list)/ { limit_req zoneanti_bot burst20 nodelay; # 其他 proxy/fastcgi 配置 }rate10r/m表示每个 IP 每分钟最多 10 次请求burst20允许瞬时最多 20 个突发请求排队。正常读者手动浏览远远达不到这个频率而采集脚本动辄每秒几十个请求直接就被拦下来了。验证规则是否生效一条命令就够了curl -I -A python-requests/2.31.0 http://yourdomain.com/article/1024如果看到403 Forbidden说明规则已经拦住。这套操作从开始到生效前后不到五分钟服务器负载肉眼可见地降了下来。但我也清楚这只是第一层对方稍微换一下 UA 就能穿过去。3.2 第二层JS 挑战与签名 Cookie拦住装成浏览器的爬虫果然止血后没两小时“小偷”就换了一套 UA伪装成 Chrome 的 UA 又来了。UA 拦截开始力不从心。这时候需要第二层让访问者证明“你真的是浏览器”。思路不复杂用户在访问详情页时如果没有一个带签名的 Cookie我们就返回一个极简页面里面有一段小 JS它会在浏览器后台执行一次本地计算把结果回传给后端后端校验通过后种下 Cookie在有效期内放行。用 Python 伪代码表示核心逻辑import hmac import hashlib import time secret your-secret-here def gen_cookie(ip, ua): # 把 IP、UA 和当前五分钟时间窗口绑在一起生成 HMAC 签名 msg f{ip}|{ua}|{int(time.time() // 300)} sig hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest() return f{msg}|{sig} def check_cookie(req): cookie req.cookies.get(site_pass) if not cookie: return False parts cookie.rsplit(|, 1) if len(parts) ! 2: return False msg, sig parts return hmac.compare_digest( sig, hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest() )核心点在于“签名”。恶意脚本如果只拿到一个空 Cookie 伪造不出正确的 HMAC而普通的 Python 采集脚本默认不会执行页面里的 JavaScript所以它根本走不到第二步。这层的真实价值是把“只改 UA”就能绕过的门槛抬升到了“必须完整实现浏览器逻辑”的难度。但这里有一个非常关键的取舍必须提醒你搜索引擎爬虫也不一定会执行 JS。如果你直接对所有流量上 JS 挑战百度、Google 的蜘蛛可能会被挡在门外收录瞬间掉光。我的做法是先把已知搜索引擎 UA 放行。给 Codex 的指令是在 JS 挑战逻辑里把百度、Google、Bing 等主流搜索引擎的 UA 放到白名单 直接通过其余未知程序一律走挑战流程。放行名单用 nginx 的 map 或者应用层判断都行但一定要单独维护一份避免将来误伤搜索抓取。3.3 第三层蜜罐陷阱与内容指纹让小偷“自证”脚本和浏览器最大的区别在于脚本会顺着页面里的所有链接一条条抓取而人类不会去点隐藏在样式里的“隐形链接”。这个行为差异就是蜜罐陷阱的基础。我在每个页面底部放了一个用 CSS 隐藏的链接普通读者完全看不到但采集程序会把它当成一个普通 URL 抓取a href/secret/honeypot-210395 styledisplay:none访问入口/a配套在 Nginx 里加一条规则location /secret/ { return 403; }任何尝试访问/secret/路径的 IP都等于“自动承认自己在抓全站链接”。我把这些 IP 单独记入日志然后批量加入封禁名单。蜜罐的好处在于它不产生任何用户体验成本静默运行对方十有八九不知道自己是怎么被识别的。第二招是内容指纹。我在每篇文章的 HTML 中嵌入一段随机生成的 token比如!-- site-watermark:7f3a91c5d2e84b0a --这段 token 没有视觉影响但它是这篇文章的“身份证”。如果对方把文章原样搬到别的站点镜像站里就会出现同样的 token。我在搜索引擎里搜一下这不常见的字符串就能立刻定位到哪个站点在盗用内容。这个技巧本来是版权溯源用的但在攻防中也非常有效它能让你判断“小偷究竟偷了多少内容、目标站是什么”。蜜罐加指纹这套组合既不打搅正常用户又能在后台持续收集证据。比起天天封 IP这个方案更像是“放长线钓大鱼”。3.4 用 Codex 写自动封禁脚本把运维从重复劳动里捞出来规则有了但如果每个小时都手动去封 IP迟早累死。这时候该让 Codex 干活了。我的需求写得很具体写一个 Python 脚本每 5 分钟扫描一次 nginx access.log 1. 访问 /secret/ 路径的 IP 直接进黑名单 2. UA 命中 python-requests、curl 等异常列表的 IP 进黑名单 3. 单个 IP 在 5 分钟内请求超过 300 次进黑名单 4. 黑名单输出到一个文件我再用 shell 同步到 nginx deny 配置和云服务商安全组。Codex 给我的第一版脚本核心逻辑大概长这样import re from collections import Counter from pathlib import Path LOG Path(/var/log/nginx/access.log) BLACKLIST Path(/opt/antibot/blacklist.txt) def suspicious_ips(lines): ips Counter() bad set() log_re re.compile( r([\d.]).*?(?:GET|POST) (\S).*? (\d) .*? (.*?) ) for line in lines: m log_re.search(line) if not m: continue ip, url, status, ua m.groups() ips[ip] 1 if honeypot in url: bad.add(ip) if python-requests in ua or curl/ in ua: bad.add(ip) for ip, cnt in ips.items(): if cnt 300: bad.add(ip) return bad def main(): lines LOG.read_text(encodingutf-8, errorsignore).splitlines() bad suspicious_ips(lines) BLACKLIST.write_text(\n.join(sorted(bad)), encodingutf-8) if __name__ __main__: main()第一版能跑但问题也不少它会把正常的高频读者误伤进去而且每次全量扫日志日志一大就慢。我继续让 Codex 迭代了两版增加了“高频率 IP 如果同时访问了多个静态资源则豁免”的规则把扫描窗口改成只看最近 1 小时并用tail -n截取避免重复计费。最终脚本稳定跑了一周误杀数量从第一天的 12 个 IP 降到了 0。部署方式也简单扔进 crontab*/5 * * * * cd /opt/antibot /usr/bin/python3 antibot.py /var/log/antibot.log 21脚本再把黑名单内容同步到 Nginx 的deny文件和云安全组的黑名单接口。到这里整条自动化封禁链路已经跑通不再需要我半夜爬起来手动操作。4. 攻防拉锯与 Codex 使用实录4.1 小偷换指纹怎么办把单一 UA 规则升成综合评分第一波拦截之后“小偷”消停了几个小时。第二天它学聪明了UA 换成了 Chrome请求频率也降到每秒两三次。单纯看 UA 和频率它和正常读者已经没有明显区别。这时候我只能把判断维度扩宽。我让 Codex 帮我做了几个聚合数据把每个 IP 的以下维度拼在一张表里User-Agent 是否包含浏览器特征是否请求了静态资源图片、CSS、JS请求的 URL 序列是否严格递增访问时间间隔是否过于均匀Referer 字段是否有正常来源是否通过 JS 挑战验证。Codex 给了一条实用的建议不要执着于“一刀切”而是算一个可疑度分数。例如UA 伪装成浏览器加 0 分但完全不请求静态资源加 2 分URL 严格递增加 2 分时间间隔均匀大于 500ms 则加 1 分。分数超过 4 分才进入观察名单超过 6 分才自动封禁。这个方案比“UA 命中直接封”稳得多。实际跑下来正常读者的分数通常在 0 到 2 分之间而采集脚本因为行为模式过于机械哪怕伪装得很好分数依然会顶到 5 分以上。这让我彻底明白一个道理判断爬虫的核心不是看它“像不像人”而是看它“能不能一直像人”。脚本再聪明也很难在所有行为维度上都伪装到位。4.2 IP 池与误封风险封网段前必须想清楚的边界攻防战打到第三天“小偷”开始用分布式 IP 池单个 IP 的请求量明显下降但是整体请求量依然高于正常水平。这时候有个诱惑直接把多个相邻 IP 的整个/24网段封掉一了百了。我差点就这么干了。但 Codex 在我给它喂数据时反问我“这些 IP 除了采集流量有没有访问过你的后台、有没有下载文件、有没有访问页面停留时间较长的记录”我一查发现其中一个待封网段的某个出口 IP 竟然携带了大量正常读者特征。如果无脑封段可能有几十个正常用户被误伤——尤其是某些公司或者校园网的出口 IP本来就是一堆人共享的。最终我改成了折中方案用 ipset 管理动态封禁只封明确命中的单个 IP对疑似网段实行“限速而非封禁”ipset create antibot_trap hash:ip timeout 86400 iptables -A INPUT -m set --match-set antibot_trap src -j DROP同时在应用层把疑似网段的访问频率降到每分钟 2 次。就算误判普通读者最多觉得“页面慢了一点”而不是直接打不开。经过三天的数据回看没有发现正常读者投诉这个方案才算真正落地。这里给你一条最实在的建议**封单个 IP 是常规操作封整个网段一定要有数据支撑。**没有充分证据之前对网段做限流而不是封杀永远更安全。4.3 Codex 实战排错速查表跑 Codex 做这个项目我遇到的坑比预想多不少。这里整理成表格方便你直接排查。错误 / 场景可能原因解决动作codex auth token is unavailable未登录、token 过期或 auth 文件路径不对重新codex login检查~/.codex/auth.json是否存在cc switch local proxy failed while handling codex endpoint /responses抓包调试工具或本地转发配置导致请求走错出口关闭调试工具清空http_proxy/https_proxy环境变量重启终端model is not supported如 gpt-5.6-solcodex.json中模型名不在当前支持列表换成受支持的模型名或只配置 model_provider 让 Codex 自行映射对话上下文过长直接把大量日志塞给 Codex先用 awk/sed 聚合只把 Top N 结果喂给它Codex 生成的脚本不生效未考虑运行路径、权限或日志轮转先在小环境跑一遍检查文件权限用python3 -m py_compile做语法检查Codex 反复给同一个建议缺少约束条件明确告诉它“不能用付费服务”“不能改现有接口”让它重新给方案封禁导致正常用户投诉规则过严或误伤共享出口 IP改用限速代替封禁用 ipset 设置短时间自动过期这条速查表里的每一项都是我在这次实战中真金白银换来的。其中最容易让人卡住的就是模型名不支持和本机转发配置错误。前者是配置细节后者是环境残留多花五分钟排查就能解决。还有一个心得Codex 并不是每次都能一步到位给出完美方案。它也会反复建议同一个思路这时候你需要主动追加约束。比如我明确告诉它“不能依赖外部付费服务”“必须在现有 Nginx 基础上改”它才会收敛出更贴合我环境的方案。把它当成一个“很聪明但不太了解你现场情况的新同事”沟通越具体产出越可用。5. 写在最后一点个人体会和给新手的建议整个项目做完我最大的体会是AI 编程工具不会取代排查能力但它能极大缩短“发现问题到解决问题”的循环。以前从日志异常到判断出恶意采集者靠人肉翻日志可能要两三个小时现在让 Codex 帮你聚合、归纳、推演十分钟就能把方向指出来。真正的判断力——比如“该不该封这个网段”“要不要加 JS 挑战”——依然得靠你自己的业务经验。给新手的建议是别一上来就追求复杂系统。先做好三件事Nginx 限流、UA 拦截、一个简单的 JS 挑战。这三样已经能挡住九成的内容小偷。蜜罐和自动封禁脚本等核心规则稳定后再逐步加进去。网站防护和写代码一样先跑通再优化比一上来就搭大架子可靠得多。最后再分享一个小技巧。我让 Codex 干活时的固定开场白是“先给我 3 个方案并按推荐优先级排序先不要写代码。”这能避免它直接跳进实现细节也方便我在多个方案里挑出最适合自己环境的那个。这次对抗网站“小偷”靠的其实不是我比对方聪明而是我多了一个能快速试错、随手写脚本的 AI 搭档。如果你手上也有一台正在被爬虫折磨的服务器不妨从今天的文章里挑一条规则开始抄作业然后慢慢迭代成适合自己站点的防护体系。