# 保哥笔记 — Nginx > 本分片含 3 篇文章,按发布日期倒序。全部分片索引见 https://zhangwenbao.com/llms-full.md **站点**:https://zhangwenbao.com/ **分类**:Nginx **生成**:2026-06-04 23:09:29 CST --- ## Nginx拦AI爬虫与限速怎么不误伤GoogleBot? - URL:https://zhangwenbao.com/nginx-ai-bot-blocking-rate-limit-rdns-misblock-account.html - 分类:Nginx - 发布:2025-04-22 | 更新:2026-05-28 - 摘要:独立站用Nginx拦AI爬虫,做完robots和UA黑名单就稳了?过去22周有三个客户站因为UA字符串变化或limit_req阈值过严误伤了GoogleBot,损失抓取预算与收录。本文把真正稳的Nginx五维组合拆开讲,再把22周五站的误伤账本横向对照,最后给12步上线SOP。 - 关键词:Nginx反爬,limit_req,rDNS反查,GoogleBot误伤,AI爬虫拦截 > **TLDR**:摘要:独立站拦AI爬虫别照搬robots黑名单,过去22周里我跟进的5个站做Nginx反爬,至少有3个站因为UA字符串变化或limit_req阈值调得太狠而误伤过GoogleBot;真正稳的不是单一拦截规则,而是5个配置位组合落地——白名单优先、UA校验只防小爬虫、limit_req阈值按抓取预算分桶、rDNS反向解析锁死大厂蜘蛛、access_log重打标签每周复盘。本文把这5维拆开讲,再把22周5个客户的误伤账本横向对照一遍,最后给12步上线SOP与5个反信号判断要不要做。 > 摘要:独立站拦AI爬虫别照搬robots黑名单,过去22周里我跟进的5个站做Nginx反爬,至少有3个站因为UA字符串变化或limit_req阈值调得太狠而误伤过GoogleBot;真正稳的不是单一拦截规则,而是5个配置位组合落地——白名单优先、UA校验只防小爬虫、limit_req阈值按抓取预算分桶、rDNS反向解析锁死大厂蜘蛛、access_log重打标签每周复盘。本文把这5维拆开讲,再把22周5个客户的误伤账本横向对照一遍,最后给12步上线SOP与5个反信号判断要不要做。 ## Nginx拦AI爬虫的5维到底是哪五维? 开门见山:拦AI爬虫这事,市面上大多数教程都把robots.txt写在第一位,把UA黑名单写在第二位,把WAF写在第三位,听起来层次分明,但落到Nginx配置层基本不可执行——robots是君子协定AI爬虫多数不读,UA黑名单一个版本号变化就漏,WAF是大锤砸鸡蛋。 我这两年在5个客户站做Nginx反爬,把所有踩过的坑整理出来,最后真正能跑稳的是5个配置位组合: - 第1维白名单:把GoogleBot、Bingbot、Baiduspider这类必须放行的大厂蜘蛛按IP CIDR + UA双向校验放进白名单,这是优先级最高的一层,所有后续规则都要让路。 - 第2维UA校验:用正则匹配关键UA模式(不是全字符串匹配),只用来挡掉那些会主动表明身份的中小爬虫——AhrefsBot、SemrushBot、各种Python requests默认UA、scrapy默认UA、curl默认UA。 - 第3维limit_req阈值:按业务流量画像和蜘蛛分桶配速率限制,搜索引擎大厂一桶、AI爬虫一桶、普通用户一桶、未知爬虫一桶,每桶独立zone独立阈值,挡DDoS也挡乱爬。 - 第4维rDNS反向解析:对宣称自己是GoogleBot/Bingbot的请求做PTR反查 + 正向解析双闸验证,这是Google官方推荐的唯一能识别假UA的方法,假冒IP直接退化为未知爬虫桶限速。 - 第5维log归因:access_log自加 $bot_tag、$rdns_verified、$rate_bucket三个字段,每周用GoAccess或者Loki+Grafana看清楚谁被挡了、谁误伤了、谁漏过了,5维配置不归因等于盲拳。 5维之间不是并列关系,是一条流水线:请求进来先过白名单(命中直接放)、不命中走UA校验(命中坏UA直接403)、再走limit_req分桶限速、宣称大厂的额外做rDNS验证、最后所有动作都打进access_log。下面5个H2把每一维拆开讲清楚。 ## Nginx配置位1 — 白名单怎么写才不漏GoogleBot? 白名单是5维里优先级最高的一道,但也是被低估最严重的一道。很多客户站的Nginx配置里没有白名单,直接deny by UA把Googlebot也按“非常规流量”挡了,然后在GSC里看到爬取错误率飙升才反应过来。 白名单要满足两个条件才能真正不漏:第一是IP CIDR来源必须用Google/Bing官方发布的IP段,不是抄某个博客贴出来的列表;第二是要和UA做双向校验——光验IP不验UA容易把代理服务器误判,光验UA不验IP容易被假冒身份骗。 Google现在每天会更新googlebot.json(在developers.google.com路径下),Bing也维护一份类似的bingip2.json,国内Baiduspider没有官方JSON但PTR域名后缀稳定(.baidu.com与 .baidu.jp)。我的做法是写一个每天04:00跑的cron,把这两份JSON拉下来转成Nginx的geo模块加载格式: geo $is_google_ip { default 0; 66.249.64.0/19 1; 34.100.182.96/28 1; 34.101.50.144/28 1; 34.118.254.0/28 1; # ...动态生成 } map $http_user_agent $is_google_ua { default 0; ~*googlebot 1; ~*googlebot-image 1; ~*googlebot-video 1; ~*googleother 1; } map $is_google_ip$is_google_ua $google_verified { default 0; “11” 1; } 关键是最后那个map:只有IP和UA同时命中才算验证通过,单独一个命中算可疑。可疑请求走rDNS二次验证(第4维),通过就放行不通过降级到未知爬虫桶限速。 5个客户站22周里白名单维护频率的真实分布: 客户型 | 白名单刷新频率 | 触发事件 | DTC美妆 | 每周一次自动 | Google IP范围每月新增2段 | B2B SaaS | 每月一次手动 | Bing IP半年更新一次 | 外贸建材 | 每季度一次 | 主要靠PTR反查不依赖IP白名单 | 跨境母婴 | 每周一次自动 | Baidu与360 IP段变动较频繁 | Shopify服饰 | 不维护 | 站在Shopify上Nginx不可控 | 反直觉的一点:白名单不是写一次就完事,IP段每月都在变。我见过最离谱的一个站,三年前的白名单文件原封不动用到2025年,里面一半IP段Google已经退了改新段,导致新段被误挡近30%——GSC报“Server error 5xx”了半年没人定位到Nginx。规则=白名单必须自动化更新,手工维护的白名单 = 定时炸弹。 ## Nginx配置位2 — UA校验为什么会误杀新爬虫? UA校验是5维里看起来最简单、其实坑最深的一道。简单是因为一行 `if ($http_user_agent ~* “ahrefsbot|semrushbot”) { return 403; }` 就能跑,深坑是因为UA字符串是爬虫开发者随时可以改的,写死任何版本号都会被下一版升级绕过。 过去两年里AI爬虫UA变化历史,整理成一张表能看清问题: 爬虫 | 初版UA | 当前UA | 变化时间 | OpenAI GPTBot (https://platform.openai.com/docs/bots) | GPTBot/1.0 | GPTBot/1.2 | 2024-08 1.0升1.1,2025-02升1.2 | OpenAI ChatGPT-User | 无 | ChatGPT-User/2.0 | 2024-04新增 | Anthropic ClaudeBot | ClaudeBot | ClaudeBot/1.0 | 2024-07加版本号 | Anthropic Claude-User | 无 | Claude-User/1.0 | 2024-11新增 | Perplexity | PerplexityBot/1.0 | PerplexityBot/1.0 + Perplexity-User/1.0 | 2024-10拆两个 | Common Crawl (https://commoncrawl.org/big-picture/frequently-asked-questions/) | CCBot/2.0 | CCBot/2.0 | 不变 | Apple Applebot | Applebot/0.1 | Applebot-Extended | 2024-06拆AI训练专用 | 这张表的启示是:写死版本号的规则(比如 `~* “gptbot/1\.0”`)半年内100% 失效,AI训练用专用UA(如Applebot-Extended)和爬通用网页的UA拆开了你的旧规则盖不住新拆出来的那个。 我的写法是用宽松正则只匹配主品牌名,不匹配版本号: map $http_user_agent $bot_class { default “human”; ~*(googlebot|bingbot|baiduspider|yandexbot|duckduckbot) “search”; ~*(gptbot|chatgpt-user|claudebot|claude-user|perplexitybot|perplexity-user|applebot) “ai”; ~*(ahrefsbot|semrushbot|mj12bot|dotbot|petalbot|bytespider) “seo-tool”; ~*(python-requests|scrapy|curl|wget|go-http-client|java-http-client) “script”; } 分类完了后用 $bot_class走不同的limit_req zone(第3维),不要直接deny by class——直接deny的话每次新爬虫出现都要改配置reload,灰度成本高。把分类和限速解耦后只需要每季度更新一次正则就行。 反直觉的一点:UA校验在5维里是最弱的一道,因为它是爬虫开发者主动配合才有效。写正则的目的不是“挡AI爬虫”,而是“给请求打标签”——真正决定挡不挡是后面的limit_req阈值和rDNS反查。保哥见过一个客户在UA层deny了所有AI爬虫然后抱怨ChatGPT搜不到自己的内容,问他“那GPTBot你想不想让它进来”,他愣了——他根本没想过AI训练(数据集采集)和AI推理(实时搜索)是两个UA。规则=UA校验只用来打分类标签,不直接做拦截动作。 ## Nginx配置位3 — limit_req阈值怎么调才不误伤抓取预算? limit_req (https://nginx.org/en/docs/http/ngx_http_limit_req_module.html)是Nginx反爬的核心武器,但90% 的客户配置都犯一个共同错误:给所有请求一套阈值。一套阈值的结果是要么对GoogleBot太严(误伤抓取预算)、要么对乱爬太松(CPU跑满)。正确做法是按上一维 $bot_class分桶: limit_req_zone $binary_remote_addr zone=human:10m rate=100r/s; limit_req_zone $binary_remote_addr zone=search:10m rate=20r/s; limit_req_zone $binary_remote_addr zone=ai:10m rate=5r/s; limit_req_zone $binary_remote_addr zone=seo_tool:10m rate=1r/s; limit_req_zone $binary_remote_addr zone=script:10m rate=1r/m; map $bot_class $rate_zone { default “human”; “search” “search”; “ai” “ai”; “seo-tool” “seo_tool”; “script” “script”; } server { location / { limit_req zone=$rate_zone burst=20 nodelay; # ... } } 5个分桶的阈值不能随便定,要按业务画像反推。我给客户算阈值的公式是: 搜索引擎大厂阈值 = 总页面数 × 平均更新频率 ÷(24×3600)× 安全系数3倍。比如5000 SKU站平均每页每7天更新一次,那么理论抓取请求/秒 = 5000 ÷ (7×86400) ≈ 0.008,安全系数3倍是0.024 r/s,但GoogleBot实际抓取会有突发(重新抓某个分类下全部商品),所以最终阈值定在10-20 r/s留余地。 22周里5个客户站limit_req阈值演进表: 客户型 | 初版search桶 | 当前search桶 | 初版ai桶 | 当前ai桶 | 调整原因 | DTC美妆1k SKU | 10 r/s | 15 r/s | 2 r/s | 3 r/s | 初版误伤GoogleBot抓图,放宽 | B2B SaaS 500页 | 5 r/s | 10 r/s | 1 r/s | 2 r/s | 页少抓取频次本来低,初版反而过严 | 外贸建材200页 | 5 r/s | 5 r/s | 1 r/s | 0.5 r/s | 页极少AI桶可以更严 | 跨境母婴5k SKU | 20 r/s | 30 r/s | 5 r/s | 8 r/s | 大站GoogleBot突发达25 r/s必须放宽 | Shopify服饰800 SKU | 不可控 | 不可控 | 不可控 | 不可控 | Shopify平台层不开放Nginx | 反直觉的一点:很多SEO顾问会建议“抓取预算不够就加内容、加sitemap、加内链”,但我跟踪的5个站里有 2个站SEO排名上不去的根本原因不是关键词、不是内链,是limit_req阈值定得太严 GoogleBot抓不动——只调limit_req一项30天内GSC收录数从1200涨到2800。规则=做SEO收录排查时limit_req zone配置和access_log里503/429占比要列在第一项检查清单。 ## Nginx配置位4 — rDNS反查怎么真正落地不被伪造UA骗? rDNS反向解析是5维里技术门槛最高、但效果最稳的一道。原理是:自称GoogleBot的请求来源IP,对它做PTR反查应该解析到 .googlebot.com或 .google.com域名后缀,然后对那个域名做正向解析回来必须等于原IP——这是Google官方在Search Central文档 (https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot)里写明的唯一识别真假GoogleBot的方法。 问题是Nginx原生不支持rDNS动态查询,直接在location里调DNS会阻塞worker进程。落地有3个方案: - 方案A — njs模块异步查询:用Nginx官方的JavaScript子集(njs)模块写rDNS查询逻辑,异步走resolver拿到结果。优点是原生Nginx不依赖外部服务,缺点是njs学习曲线高。 - 方案B — Lua + OpenResty:openresty自带lua-resty-dns库,几行代码搞定。优点是社区案例多,缺点是要换OpenResty不能用社区版Nginx。 - 方案C — 旁路cache验证:access_log里出现自称大厂UA的请求时,用一个Python sidecar异步做rDNS验证、把结果写进Redis缓存(TTL 24小时),下一次同IP来请求直接读缓存。优点是Nginx配置极简,缺点是首次访问没缓存时还是放行。 我的5个客户里3个用方案C、2个用方案B(OpenResty)。方案A听起来最纯但社区案例稀少调试痛苦,不推荐给非专职运维的团队。 方案C的实战配置示例: # Nginx主配置 map $bot_class$is_google_ua $needs_rdns { default 0; “search1” 1; “ai1” 1; } # log里多打一列needs_rdns log_format with_bot '$remote_addr - $remote_user [$time_local] ' '“$request” $status $body_bytes_sent ' '“$http_referer” “$http_user_agent” ' 'bot=$bot_class rdns=$needs_rdns zone=$rate_zone'; # Python sidecar每秒tail一次access_log # 对needs_rdns=1但还没缓存的IP做PTR + 正向解析双闸 # 验证不通过的IP直接加到Nginx的deny列表(include /etc/nginx/blacklist.conf;) # 验证通过的IP加到白名单geo表里 22周里5个站rDNS反查的真实成效统计: 客户型 | 22周总请求宣称大厂UA | rDNS验证通过 | 验证失败(假冒) | 失败IP来源Top 3 | DTC美妆 | 184万 | 168万 | 16万(8.6%) | .ovh.com / .digitalocean.com / 中国IDC | B2B SaaS | 52万 | 49万 | 3万(5.7%) | .amazonaws.com / .vultr.com / 中国IDC | 外贸建材 | 28万 | 26万 | 2万(7.1%) | .ovh.com / .contabo.com / .hetzner.de | 跨境母婴 | 396万 | 361万 | 35万(8.8%) | .ovh.com / .leaseweb.com / 中国IDC | 4个站平均有7-9% 的“自称GoogleBot/Bingbot”流量是假的,来源高度集中在OVH、DigitalOcean、AWS这几家便宜VPS——典型的小爬虫开发者租5美金/月机器跑脚本伪造UA。这部分流量如果不做rDNS反查全靠UA校验过滤,会全部按search桶走20 r/s阈值,单IP一天能抓170万次,相当于一个小型DDoS。 反直觉的一点:保哥早年也以为UA校验 + IP白名单已经够了,做完rDNS反查才发现假冒流量占比8% 在大站每月能多消耗30%-40% 的CPU。规则=rDNS反查在5维里是性价比最高的一道,做完前对自己的反爬体系不要有信心。日志分析与爬虫验证的深度方法 (https://zhangwenbao.com/server-log-file-analysis-seo-crawl-budget-bot-verification.html)另开一篇专讲,本文重点在Nginx配置层。 ## Nginx配置位5 — log归因怎么看清谁被挡了? log归因是5维里最容易被忽略、但回报最直接的一道。前4维都是“配置侧”的事情,log归因是“验证侧”的事情——没有归因等于盲拳,配完不知道效果。 5维落地后的log配置应该包含5个自定义字段:$bot_class(来自UA校验)、$rate_zone(来自分桶映射)、$google_verified(来自白名单)、$rdns_verified(来自旁路验证)、$req_status_at_limit(被limit_req拦下时的503/429标记)。 log_format reverse_proxy '$remote_addr [$time_local] ' '“$request” $status $body_bytes_sent ' '“$http_user_agent” ' 'bot=$bot_class zone=$rate_zone ' 'google=$google_verified rdns=$rdns_verified ' 'limit_status=$limit_req_status'; access_log /var/log/nginx/access.log reverse_proxy; 有了这套log后,每周复盘清单应该看4件事: - 第1件:被挡的合法蜘蛛。grep `limit_status=REJECTED google=1`,应该接近0;如果不是0说明search桶阈值太严,要调宽。 - 第2件:被放过的假爬虫。grep `bot=search rdns=0`,应该越少越好;如果占比大于2% 说明rDNS sidecar跑得不够频繁,要调短TTL。 - 第3件:漏伤的真用户。grep `bot=human limit_status=REJECTED`,应该接近0;如果不是0说明human桶阈值太严,要么调宽要么排查爬虫UA是不是有漏识别。 - 第4件:未知爬虫桶占比。grep `bot=script`,看趋势;上涨说明新爬虫品种在涌入要更新正则。 归因工具选哪一个看团队规模和预算: 方案 | 适合规模 | 实施成本 | 归因深度 | Excel + log切片 | < 1万PV/天 | 1小时/周 | 看主要趋势够 | GoAccess实时dashboard | 1-10万PV/天 | 30分钟搭建 | 看分桶分布够 | Loki + Grafana | > 10万PV/天 | 1-2天搭建 | 可下钻到单IP单UA | Cloudflare Logpush + BigQuery | 已用CF企业版 | 付费功能 | 跨节点跨周聚合 | 反直觉的一点:很多客户做完前4维配置后觉得“万事大吉”,3个月后流量突然下降才发现search桶里GoogleBot被误伤了30%。规则=log不归因,5维配置等于盲拳;每周复盘4件事是5维的最后一道闸。独立站Cloudflare缓存治理与回源率优化决策树 (https://zhangwenbao.com/cloudflare-cache-real-world-optimization-decision-tree.html)里有相关的log切片技巧可以借鉴。 ## 22周5站误伤账本横向对照:哪一类站点最容易误伤? 5维讲完了,下面把22周5个客户站的实战账本拉出来横向对照——同样的5维配置,在不同业务类型站上误伤率差别可以达到10倍。 客户型 | SKU/页量 | 初版误伤GoogleBot比例 | 调整后误伤比例 | SEO流量变化(4个月) | 主要踩坑维度 | DTC美妆 | 1000 SKU | 4.2% | 0.3% | + 18% | UA校验把GoogleBot-Image漏了 | B2B SaaS | 500页 | 1.1% | 0.2% | + 6% | limit_req对小站本就够松 | 外贸建材 | 200页 | 0.5% | 0.1% | 持平 | 页极少没什么爬取压力 | 跨境母婴 | 5000 SKU | 11.3% | 0.8% | + 31% | limit_req阈值定20 r/s太严 | Shopify服饰 | 800 SKU | 不可控 | 不可控 | + 2% | Shopify平台层Nginx不开放 | 横向看出几个规律: - 高SKU站(跨境母婴5000、DTC美妆1000)初版误伤最高,因为GoogleBot抓取突发量大、limit_req阈值容易卡。 - 低SKU站(外贸建材200、B2B SaaS 500)初版误伤就很低,因为本来抓取频次就低,5维配置主要价值在挡乱爬虫,不在保抓取预算。 - Shopify这类SaaS平台站根本不开放Nginx配置层,只能依赖Cloudflare或者Shopify自带的Bot Management,5维方法在这类站完全用不上。 - 调整后误伤比例普遍降到1% 以下,且SEO流量在4个月内有可观增长(高SKU站增长最显著)。 22周里把5个站分成4个阶段:第1-4周打access_log基线、第5-8周配置初版5维上线、第9-16周每周复盘调整、第17-22周稳定运行做横向对照。这个时间线在中等复杂度站点是合理的,简单站可以压缩到8-12周,超复杂多语言站可能需要30周以上。 ## 5个最容易踩的Nginx配置坑是什么? 5维配置过程中我前后踩过不止5个坑,挑出影响最大、最容易在交付时复发的5个列在下面: - 坑1 — deny指令写在location内部不生效。Nginx的ngx_http_access_module处理deny/allow的优先级是按context来的,写在server块里和写在location块里执行时机不同;如果location内部还有internal redirect(rewrite ^/ /index.php?$args last),deny会被绕过。规则=deny/allow写在server块顶部,不写location内部。 - 坑2 — limit_req zone名漏定义致全站不限速。zone必须在http块用limit_req_zone指令定义,然后在server/location里用limit_req zone=name引用;如果zone名拼错(比如定义的是 `ai_bot` 引用写成 `ai-bot`),Nginx不报错但直接不限速。规则=nginx -t后必须再跑一次reload + 看error_log里有没有zone not found警告。 - 坑3 — rDNS反查直接同步阻塞Nginx worker。Nginx默认的resolver是同步的,如果在location里用set + resolver做DNS查询,每个请求都会卡50-200ms,并发上来worker全部卡死。规则=rDNS必须走njs/lua异步模块,或者走旁路sidecar不写Nginx主路径。 - 坑4 — UA正则贪婪匹配把Mozilla全ban了。新手常写 `~* “bot”` 这种宽松正则,结果把所有包含 “bot” 的UA都按爬虫处理——比如 “Mozilla/5.0 (compatible; sneakbot/1.0)” 会命中,但更糟的是GoogleBot的UA字符串里就有 “GoogleBot” 也包含 “bot”,如果白名单的优先级没在UA校验之前GoogleBot也会被挡。规则=UA正则要精确到品牌名而不是单词 “bot”,且白名单必须先于UA校验执行。 - 坑5 — 白名单map顺序错把deny all顶进白名单。Nginx的map指令是按声明顺序匹配的,default值放在第一行还是最后一行结果不同;如果在map里写 `default 0; ~*googlebot 1; deny_all 1;` 这种顺序,deny_all会覆盖googlebot。规则=map里default永远放第一行,特殊匹配规则按品牌名分组排列。 这5个坑的共同特征是Nginx -t不报错、reload成功、表面看一切正常,问题在生产跑了1-2周才在access_log里冒头。规则=5维上线后必须做7天灰度(第9-15步SOP),不能直接全量。 ## GoogleBot、Bingbot、GPTBot、ClaudeBot、PerplexityBot真实UA与IP范围怎么校验? 白名单和UA正则要写对,第一步是把5大蜘蛛的真实UA、IP范围发布页、rDNS域名后缀整理清楚。下面这张表是我维护的对照清单,每季度刷新一次: 蜘蛛 | 主要UA模式 | IP范围来源 | rDNS后缀 | 注意陷阱 | GoogleBot | ~*googlebot | developers.google.com/googlebot.json | .googlebot.com / .google.com | GoogleBot-Image / GoogleBot-Video是独立UA,要分别匹配 | Bingbot | ~*(bingbot|msnbot) | www.bing.com的bingbot.json | .search.msn.com | BingPreview是另一个UA做手机预览不是搜索抓取 | OpenAI GPTBot | ~*gptbot | platform.openai.com gptbot文档列IP | 不公开PTR后缀,靠IP范围验证 | ChatGPT-User是实时搜索UA不是训练,要分桶 | Anthropic ClaudeBot | ~*(claudebot|claude-user|claude-searchbot) | 不公开IP范围只公开UA列表 | 不公开 | 3个UA用途不同:ClaudeBot训练Claude-User实时引用Claude-SearchBot搜索 | Perplexity | ~*(perplexitybot|perplexity-user) | 不公开IP范围 | 不公开 | PerplexityBot训练Perplexity-User实时回答 | Common Crawl | ~*ccbot | commoncrawl.org/big-picture列范围 | .commoncrawl.org | 非营利但抓取量大要单独限速 | 这张表里有几个高频陷阱要点出来: - 陷阱1 — Bingbot与Bingbot AI是不同UA。Bing在2024年拆出了独立的AI训练用UA,普通Bing搜索抓取走bingbot/2.0,AI训练走BingPreview或msnbot-news,规则要写两条。 - 陷阱2 — Anthropic不公开IP范围。ClaudeBot系列没有像Google那样的IP JSON,所以rDNS反查对Claude不可用,只能靠UA + Cloudflare AI Bots Management第三方验证。 - 陷阱3 — 通用网页抓取vs AI训练数据采集是不同UA。GoogleBot是抓页面给Google搜索用,Google-Extended是Google Gemini训练用(默认放行如果你robots.txt没禁),Applebot-Extended同理。规则=白名单只放搜索抓取那个UA,AI训练UA按业务策略决定是放是挡。 - 陷阱4 — IP范围发布页路径会变。Google 2023年把googlebot.json从google.com路径迁到developers.google.com,旧路径还能访问但不再更新;自动化拉取脚本要跟着改URL。 - 陷阱5 — rDNS后缀有多个变种。GoogleBot既可能解析到 .googlebot.com也可能到 .google.com(视IP段),正向解析回来必须等于原IP才算通过。规则=rDNS验证逻辑要支持多后缀匹配,写死单一后缀会漏。 我的客户22周里发现的“自称大厂”流量来源分布前面的表已经列过,这里补一个:除了真假GoogleBot之外,还有一类需要单独识别——伪装成普通用户的headless Chrome爬虫,UA里完全没有bot字样,靠UA校验完全过滤不了,只能靠JS Challenge(Cloudflare Bot Management或者自建Turnstile)做行为校验。这部分逻辑不在Nginx 5维范围内,拦AI爬虫该不该的三层选型框架 (https://zhangwenbao.com/block-ai-bots-robotstxt-waf.html)里有更细的分层方法。 ## 不同业务客户怎么选拦截组合?6类客户决策树 5维不是每个站都要全配,按业务类型选组合才划算。保哥按22周5个客户站+此前接触的20+客户案例整理出6类决策树: - 类1 — 纯展示官网(< 100页):只需要白名单 + UA校验2维,limit_req阈值随便定一个保底值就行,rDNS反查不必上,log归因每月看一次。理由是抓取压力本来就小,过度配置不划算。 - 类2 — 中小DTC(500-1000 SKU):5维全配但limit_req阈值给得宽松,rDNS走旁路方案C不挂主路径,log归因每周一次。理由是GoogleBot抓取频次中等,误伤代价中等。 - 类3 — 大DTC(5000+ SKU):5维全配且limit_req阈值要按蜘蛛分桶细分到至少6个zone(search、ai-train、ai-realtime、seo-tool、script、human),rDNS必须上方案A或B实时验证,log归因每天一次。理由是抓取预算是SEO收录的硬瓶颈,误伤代价巨大。 - 类4 — B2B资讯站(页中等量):白名单 + UA校验 + limit_req 3维即可,rDNS反查可以跳过,log归因每月一次。理由是流量来源以直接访问和内链为主,搜索流量占比不主导。 - 类5 — SaaS文档站(万级页):5维全配且要为GoogleBot-Image、GoogleBot-News等子UA单独留zone,rDNS必须上,log归因每周一次。理由是文档站搜索流量占比极高,且GoogleBot多种子UA同时活跃。 - 类6 — Shopify/Wix等SaaS平台站:5维全部不可控,转走平台层Bot Management(Shopify用Cloudflare、Wix用平台自带)+ Cloudflare企业版的AI Audit。理由是平台层Nginx不开放,配置层5维方法不适用。 决策树的逻辑入口是三个问题:第1问页面数(< 100 / 100-1000 / 1000-10000 / > 10000)、第2问平台(自有服务器vs SaaS平台)、第3问搜索流量占比(< 30% / 30-70% / > 70%)。三个答案组合出6类的归属。 反直觉的一点:很多顾问会建议“所有站都配5维”,但中小站做完5维的维护成本(每周复盘1小时 + 每月白名单同步1小时 + 每季度UA正则更新1小时)一年累计40+小时人力,对纯展示官网而言收益远低于成本。规则=5维是工具不是目标,按业务收益反推决定配几维。 ## 12步Nginx反爬上线SOP怎么走? 5维选定后落地按12步SOP走,每一步都有验收点不能跳: - 第1步 — access_log 1周打底。开启完整access_log记录现状抓取分布,不做任何拦截。验收=至少7×24小时数据。 - 第2步 — 识别现有蜘蛛UA分布。用awk+sort+uniq切出Top 50 UA,标记哪些是大厂、AI、SEO工具、脚本爬虫。验收=识别覆盖率 > 95%。 - 第3步 — 分桶5类。按UA分类对应到search/ai/seo-tool/script/human 5桶,剩余未识别归human桶(保守策略)。验收=5桶覆盖100% 流量。 - 第4步 — 白名单配置IP+UA双向。从google/bing官方JSON拉IP段配geo模块,map UA正则做双向校验。验收=nginx -t通过 + 测试GoogleBot UA模拟请求200。 - 第5步 — UA校验正则。第2维map配置,分类到 $bot_class。验收=测试用例覆盖5桶各至少3个UA样本。 - 第6步 — limit_req zone定义。第3维5个zone按业务画像反推阈值。验收=nginx -t通过 + ab压测各zone阈值符合预期。 - 第7步 — rDNS njs模块。第4维落地(方案A/B/C选其一)。验收=模拟ovh.com IP自称GoogleBot应被识别为假冒。 - 第8步 — log重打tag。第5维log_format注入5个自定义字段。验收=access_log新格式可读且GoAccess能解析。 - 第9步 — 灰度10% 流量。用split_clients或者Nginx upstream weight把10% 流量切到新配置,90% 留在旧配置做对照。验收=灰度组与对照组流量分布无显著差异。 - 第10步 — 观察7天误伤率。grep limit_status=REJECTED google=1 / bot=human limit_status=REJECTED两类异常。验收=两类异常占比均 < 1%。 - 第11步 — 全量上线。把灰度比例从10% 拉到100%,旧配置保留30天作回滚备份。验收=GSC抓取错误率7天内不升高。 - 第12步 — 每周复盘。按第5维log归因的4件事清单做每周1小时复盘,发现异常调阈值。验收=持续运行不少于8周。 12步走完整套流程在中等复杂度站点(1000-5000 SKU)大约需要5-7周,简单站可以压缩到3-4周,复杂站(多语言 + 多业务线)可能10+周。规则=SOP不能跳第9-10步灰度,直接全量上线一旦误伤GoogleBot需要2-4周才能恢复GSC收录信号。 ## 什么情况下别做Nginx拦截?5个反信号 5维方法有适用边界,下面5个反信号出现就不要做,配了反而是负面收益: - 反信号1 — 站点流量低于1000 UV/天。爬虫流量绝对值很小,5维带来的运维负担超过收益。建议=robots.txt + 简单UA黑名单足够。 - 反信号2 — 已经在用Cloudflare企业版的Bot Management。CF的Bot Management比Nginx 5维更细,且不消耗自有服务器资源;Nginx层再做5维等于重复劳动还可能产生规则冲突。建议=CF配为主,Nginx只做最低限度的回源保护,AI爬虫流量趋势可以横向看Cloudflare Radar (https://radar.cloudflare.com/)的公开数据印证。 - 反信号3 — 团队没有专职运维定期看log。5维上线后第12步每周复盘是关键,没人看log等于配了个定时炸弹——3个月后IP段变了、新爬虫出现了,全部默默失效。建议=按月外包给一个能看log的顾问,或者退回到反信号1的简化方案。 - 反信号4 — 站点正在主动加入AI数据共享。如果业务策略是AI时代主动让GPTBot/ClaudeBot抓取以换取AI回答里的引用,那5维里ai桶应该完全放行,不应该限速。建议=把5维改成“4维 + ai桶完全白名单”。 - 反信号5 — 站点是纯销售线索B2B流量来源已稳。流量不是KPI(销售线索是),SEO抓取量的小幅波动不影响销售管线,5维上线的投入回报比低。建议=robots.txt加GPTBot Disallow(如果不想被训练)+ Cloudflare免费版Bot Fight Mode已足够。 反信号判断的核心逻辑是:5维方法的真正价值在“保抓取预算 + 控未知爬虫资源 + 反假冒身份”三件事,如果业务上这三件事都不是关键瓶颈,那5维就是过度工程。 保哥见过最浪费的一个案例:一个日均200 UV的纯展示官网,团队花了3个月配完5维结果第4个月就因为没人维护IP段过期了GoogleBot被挡35%,最后干脆把5维全删了回到robots.txt + 简单黑名单。规则=先按反信号自检,过反信号闸再决定上5维。Apache .htaccess SEO 6层综合治理 (https://zhangwenbao.com/apache-htaccess-seo-6-layer-rewrite-cache-canonical-hsts.html)那篇里有Apache视角的反信号清单可以横向对照。 ## 常见问题解答 ## Nginx 5维拦AI爬虫和直接用robots.txt区别有多大? robots.txt是君子协定,所有AI爬虫里只有GoogleBot严格遵守,GPTBot/ClaudeBot/PerplexityBot在2024-2025年都有过被实测无视robots.txt的记录。Nginx 5维是物理层拦截,不依赖爬虫主动配合。两者关系:robots.txt表达意图(给守规矩的爬虫看),Nginx 5维强制执行(给不守规矩的爬虫挡)。规则=两者都要有,不是二选一。 ## 做完5维后Cloudflare还需要开Bot Management吗? 看用CF哪一档。CF免费版的Bot Fight Mode只挡基础爬虫,与Nginx 5维冲突小可以共存;CF Pro版的Super Bot Fight Mode与5维有功能重叠但CF是边缘节点先过滤、Nginx是回源前再过滤,纵深防御对大站有意义;CF企业版的Bot Management比Nginx 5维细很多,那Nginx层可以简化只保留白名单和limit_req兜底。规则=按CF档位决定Nginx 5维做几维。 ## rDNS反查会不会增加首次请求的延迟? 会,且不可忽略。直接同步rDNS在Nginx worker里查询,单次延迟50-200ms取决于DNS服务器响应。所以本文推荐方案C(旁路sidecar)——首次请求不做实时验证,先按UA校验放行,sidecar异步验证完写Redis,第二次请求开始才走完整rDNS路径。代价是首次请求可能放过1次假爬虫,但避免了主路径阻塞。规则=rDNS验证必须异步,主路径不能等同步DNS。 ## 5维上线后GSC抓取统计应该看哪些指标? 看4个指标:第1是抓取请求总数(应该稳定或略升)、第2是平均响应时间(应该不变或略降)、第3是抓取错误率(应该不升)、第4是按响应状态分布(5xx/429占比应该接近0)。前4周抓取请求总数可能小幅波动(GoogleBot适应新配置),4周后应该稳定。如果4周后抓取请求总数比上线前低20% 以上,大概率是limit_req阈值定得太严,要回去调宽。 ## 不同业务类型站做完5维SEO流量都能涨吗? 不一定。高SKU站(5000+ SKU)做完5维后4个月SEO流量平均涨20-30%,因为抓取预算之前被limit_req卡死;中等SKU站(500-1000 SKU)涨幅5-15%;低SKU站(< 200页)几乎不涨,因为抓取预算本来就不是瓶颈。规则=5维的SEO收益与站点规模正相关,小站做5维收益主要在挡乱爬虫节省CPU不在涨流量。 ## 5维方法在国内服务器(阿里云、腾讯云、宝塔面板)上有什么特殊要求? 3点特殊要求:第1是Baiduspider与360Spider的IP范围没有像Google那样的官方JSON,要靠PTR反查 .baidu.com / .so.com后缀验证;第2是宝塔面板的Nginx配置在vhost文件里改了之后宝塔会自动覆盖,必须用include /etc/nginx/conf.d/custom.conf的方式把5维配置写在vhost之外;第3是阿里云/腾讯云的安全组默认会限速一部分高频请求,做5维之前要确认云厂商安全组的限速规则不会与Nginx limit_req冲突。 ## 权威参考资料 ## DedeCMS目录禁PHP解析加固:nginx与Apache配置加文件权限 - URL:https://zhangwenbao.com/nginx-dedecms-php-deny-all.html - 分类:Nginx - 发布:2021-09-16 | 更新:2026-06-02 - 摘要:织梦被传webshell,多半是上传目录还能解析PHP。本文给出安全加固:用一段nginx的location正则deny all屏蔽uploads、templets、data等目录的PHP解析,附Apache等效写法、权限矩阵、open_basedir与disable_functions加固和应急清理流程。 - 关键词:宝塔面板,目录权限,DedeCMS安全,Nginx配置,webshell防护 > **TLDR**:摘要:织梦被传webshell,多半是上传目录还能解析PHP。本文给安全加固——用一段nginx的location正则deny all屏蔽uploads、templets、data等目录的PHP解析,附Apache等效配置、文件系统权限配合,再讲除了禁PHP还要做的加固、配置验证、被攻击后的应急、典型攻击案例复盘和与ModSecurity与Cloudflare WAF的分工。 > 摘要:织梦被传webshell,多半是上传目录还能解析PHP。本文给安全加固——用一段nginx的location正则deny all屏蔽uploads、templets、data等目录的PHP解析,附Apache等效配置、文件系统权限配合,再讲除了禁PHP还要做的加固、配置验证、被攻击后的应急、典型攻击案例复盘和与ModSecurity与Cloudflare WAF的分工。 2014 年保哥接手过一个三天两头被挂马的织梦 DedeCMS 站点,每次清理完没几天又被植入新马,最后排查下来根本原因不在 DedeCMS 自身的漏洞,而是攻击者通过编辑器上传图片接口把 木马.php.jpg 这种伪装文件丢到 /uploads/ 目录,再通过路径直接访问 uploads/xxx/shell.php 执行恶意代码。从那以后保哥养成了一个固定习惯:所有 DedeCMS 站点上线第一件事,就是把 uploads、templets、images、html 这类不需要执行 PHP 的目录在 nginx 层 deny 掉 PHP 解析。这一道防线挡住了 80% 以上的 PHP 后门攻击。本文记录这套方案的完整配置、攻击原理、相关安全加固的全套细节。 ## 为什么要禁止特定目录执行 PHP 织梦 DedeCMS 这类老 CMS 之所以挂马频繁,根本原因是"凡是能被上传文件的目录都可能被植入 PHP 后门"。攻击者常见手法有以下几种: - 编辑器漏洞上传。FCKEditor、UEditor、CKEditor 等富文本编辑器历史上有过若干上传校验绕过漏洞,攻击者通过构造特殊的 Content-Type、Magic Number 把 PHP 文件伪装成图片上传到 /uploads/。 - 文件管理器越权。后台文件管理器如果鉴权不严,攻击者拿到普通用户账号也能传 PHP 到任意目录。 - 会员投稿接口。开放注册的站点,会员投稿往往附带"上传图片"功能,逻辑漏洞同样可能被利用。 - 第三方插件。织梦插件生态早期审核宽松,部分插件自带的上传组件就是后门入口。 - 历史遗留。多年前被入侵留下的后门可能还潜伏在某个图片目录里,没被清干净。 这些攻击手法的共同特征是:把 PHP 代码丢到一个本不该执行 PHP 的目录。如果 web 服务器看到 /uploads/xxx.php 直接交给 PHP-FPM 解析,攻击就成功;如果在 nginx 层就拦截掉,deny all 返回 403,攻击就失败。这就是本文方案的核心思路:白名单思维——只在确实需要 PHP 解析的目录开放,其他目录一律禁止。 ## 宝塔面板 nginx 完整配置 登录宝塔面板 (https://zhangwenbao.com/bt-panel-upgrade-failed.html),进入对应站点的"配置文件"编辑界面(也可以直接 SSH 编辑 /www/server/panel/vhost/nginx/yourdomain.conf)。在 server { ... } 块内、紧挨着 root 行下面,加入以下配置: location ~* ^/(include|uploads|a|templets|skin|images|html|data|special)/.*\.(php|php5|php7|phtml|pht)$ { deny all; return 403; access_log off; log_not_found off; } 这段配置的几个关键细节: - ~* 是大小写不敏感的正则匹配,能拦住 .PHP、.Php 这类大小写变体。 - ^/(...)/ 限定只匹配以这些目录名开头的路径。如果你的站点这些目录有别名或软链,需要把别名也加进来。 - \.(php|php5|php7|phtml|pht)$ 覆盖 PHP 解析器可能识别的所有扩展名。.phtml 和 .pht 是历史上被绕过验证常用的扩展,强烈建议一并屏蔽。 - deny all + return 403 双保险,确保不会因为某些 nginx 版本的解析差异让请求漏过去。 - access_log off 减少日志噪音,因为这类请求一旦发生通常是攻击探测,记下来意义不大反而消耗磁盘。 保存配置后宝塔会自动 reload nginx,几秒钟内生效。如果是手动改的 conf 文件,记得 nginx -t 测试语法后 nginx -s reload。 ## 目录清单与责任分工 织梦 DedeCMS 默认目录结构里,建议禁 PHP 的目录及其用途: - /uploads/:用户上传的图片、附件、文档。100% 不应该执行 PHP。 - /templets/:前端模板文件目录。模板里只有 .htm、CSS、JS、图片,不需要 PHP 解析(DedeCMS 的模板渲染是在编译阶段完成的,渲染后的 HTML 输出文件在别的地方)。 - /skin/:后台皮肤资源(CSS、JS、图片)。 - /images/:站点公用图片资源。 - /html/:DedeCMS 静态生成的 HTML 文件目录(如果开启了静态生成)。 - /a/:DedeCMS 默认的文章静态生成根目录(取决于站点配置)。 - /include/:核心 PHP 文件,但对外不应该暴露访问——所有调用都通过其他入口文件 require_once 引入。所以禁止外部直接访问 /include/xxx.php 是合理的。 - /data/:缓存、备份、临时数据。绝对不能执行 PHP,因为这里有数据库备份文件、敏感配置缓存。 - /special/:专题页目录(如果用了专题功能)。 需要保留 PHP 执行的目录有:根目录入口文件(index.php、list.php、view.php、tag.php 等)、/dede/ 后台目录(建议改名)、/member/ 会员中心、/plus/ 扩展目录(如果在用)。 ## Apache 环境的等效配置 如果站点跑在 Apache 上,可以通过 .htaccess 实现同样效果。在 /uploads/、/templets/ 等需要禁 PHP 的目录里,分别放一份 .htaccess: Order Deny,Allow Deny from all Apache 2.4 及以上推荐用新语法: Require all denied Apache 的 .htaccess 方案有两个优势:一是粒度细,每个目录单独控制;二是修改不需要 reload web 服务。劣势是每次请求都会读 .htaccess 文件,性能不如 nginx 集中配置好。如果你的站点流量大且仍在用 Apache,建议把规则迁到主配置文件里减少 .htaccess 解析开销。 ## 文件系统权限配合 nginx 层的 PHP 解析屏蔽是第一道墙,文件系统权限是第二道。两道叠加才稳。织梦 DedeCMS 推荐的目录权限: - 网站根目录:755(所有者读写执行,其他用户读执行)。 - /data/:750(仅 owner + group 可访问,禁止 other 读取,避免 data/sqldata/*.txt 数据库备份被外网爬走)。 - /uploads/:755(PHP-FPM 进程要能写入上传文件)。 - /dede/:755 或 750(后台代码,建议改名为非默认 dede 后再设权限)。 - /include/:755。 - 所有 PHP 文件:644(owner 读写,其他读,禁止任何用户写——攻击者就算上传了 PHP 也无法覆盖现有文件)。 - 所有目录:755(保证 web 进程能进入)。 批量设置命令(在站点根目录执行): find . -type d -exec chmod 755 {} \; find . -type f -exec chmod 644 {} \; chmod -R 750 ./data/ chown -R www:www ./ 注意 owner 要是 web 服务用户(宝塔默认 www,CentOS 默认 nginx 或 apache)。如果上传写入失败,多半是 owner 错了,chown 修一下即可。 ## 加固清单:除了禁 PHP 还要做的 禁 PHP 是基础线,下面这几项加上去防御就比较完整了: - 后台目录改名。把 /dede/ 改成不规则的字符串如 /admin_a8x7k2/,再配合 IP 白名单访问,能挡住绝大多数自动化扫描。 - install.php 删除或断网。安装完成后 install/index.php.bak 文件务必删掉或加 deny all,历史上有大量站点是因为 install 文件没删被攻击者重装。 - plus 目录限制。/plus/ 下很多文件是历史漏洞重灾区(如 recommend.php、flink_add.php),不需要的功能直接删掉对应文件。 - 禁用 PHP 危险函数。在 php.ini 里 disable_functions 加上 exec, passthru, shell_exec, system, proc_open, popen, eval 等,即使有后门也无法执行命令。 - open_basedir 限制。把 PHP 进程能访问的文件路径限制在站点根目录内,攻击者无法跨站读 /etc/passwd。 - WAF 接入。宝塔自带的 nginx 防火墙、阿里云 WAF、Cloudflare WAF 任选其一,对常见攻击 payload 做规则拦截。 - 文件完整性监控。AIDE、tripwire 或宝塔的"文件监控"功能,对 PHP 文件做哈希校验,新增/修改文件立即告警。 - 定期备份。代码 + 数据库每周完整备份,备份保留至少 30 天滚动,避免被攻击后无干净版本可恢复。 ## 验证配置是否生效 配置完成后必须验证一遍是否真的生效。验证步骤: 第一步,SSH 登录服务器,在 /uploads/ 目录创建一个测试 PHP 文件: echo "" > /www/wwwroot/yoursite/uploads/test_check.php 第二步,用浏览器访问 https://yoursite.com/uploads/test_check.php。如果返回 403 Forbidden,说明 nginx 配置生效了;如果返回 phpinfo() 页面,说明配置没生效,需要回查 nginx 配置和 reload 状态。 第三步,访问 /uploads/somepic.jpg 类正常图片,确保 nginx 没误伤——只有 .php 文件被屏蔽,其他文件类型应该正常返回。 第四步,测试完务必删除测试文件:rm /www/wwwroot/yoursite/uploads/test_check.php,避免留隐患。 第五步,把这个测试用例做成脚本,每次 nginx 配置变更后自动跑一次,避免下次有人改 nginx 时把这条规则注释掉了没人知道。 ## 被攻击后的应急处理 如果你接手的站点已经挂马了,光配 nginx 不够,得先清干净。保哥的应急清理流程: - 立刻断网。在防火墙层把 80/443 端口封掉,阻止攻击者持续访问后门。 - 全站文件备份。打 tar 包带走,留作取证。 - 找出后门文件。用 grep -rEn "(eval|base64_decode|gzinflate|str_rot13|assert)\(" /www/wwwroot/yoursite/ 扫描含可疑函数的 PHP 文件,逐个人工确认。 - 检查 webshell 特征。常见后门特征:单一文件含完整 PHP 入口 + 加密通信 + 文件管理 + 命令执行;用 find 按修改时间筛最近变更的文件 find /www/wwwroot/yoursite -name "*.php" -mtime -7。 - 对比官方代码。从 DedeCMS 官方下载同版本干净源码,diff 出所有被改动的文件。 - 清理数据库。后门也可能藏在数据库里(管理员表、文章正文 onload 注入),逐表 SELECT 高危关键字。 - 重置所有密码:管理员账号、数据库密码、SSH 密钥、宝塔面板密码。 - 部署本文配置。nginx deny PHP + 文件权限收紧 + WAF 上线。 - 恢复网络访问,但先内网测试 24 小时确认没有残留后门。 ## 其他 CMS 的等效配置参考 这套思路并非 DedeCMS 独占。其他主流 CMS 的"禁 PHP 目录清单"参考: - WordPress:wp-content/uploads/、wp-content/themes/*/assets/ 等需要禁 PHP。 - Typecho:usr/uploads/、usr/themes/*/assets/ 需要禁 PHP。 - Discuz (https://zhangwenbao.com/discuz-global-variables-details.html):data/attachment/、uc_server/data/、data/cache/ 需要禁 PHP。 - ECShop (https://zhangwenbao.com/ecshop-prompts-deprecated-preg_replace-to-report-incorrect-solutions.html) / ECTouch (https://zhangwenbao.com/ecshop-mobile-ectouch.html):images/、data/、themes/ 需要禁 PHP。 - 帝国 CMS:d/file/、d/txt/、d/js/、e/template/ 需要禁 PHP。 - Drupal / Joomla:sites/default/files/、images/、media/ 需要禁 PHP。 所有 CMS 的逻辑都一样:"上传目录、模板资源目录、缓存目录绝对不允许执行 PHP",这是通用安全准则。 ## 监控告警与持续运维 配置完成不代表万事大吉,建议长期开启以下监控: - nginx access.log 异常 PHP 请求统计。每天用 awk 统计 /uploads/.*\.php 类请求数量,突然飙升说明有人在扫描。 - 403/404 突增告警。如果一夜之间 403 数量从几十涨到几千,多半遇到了自动化攻击。 - 文件改动告警。inotify-tools 或宝塔的文件监控对 /uploads/ 目录做监听,新增 .php 文件立即告警。 - 登录日志。后台管理员登录、SSH 登录、宝塔面板登录,全部接入告警通道(邮件/钉钉/企业微信)。 - 季度安全审计。每季度跑一次 find / -newer /tmp/last_audit -type f 找出近期修改的所有文件,过一遍是否合规。 这些监控加上去,攻击发生时能在 5-30 分钟内发现并响应,远比"被挂马半年才发觉"强得多。 ## 典型攻击案例复盘 保哥这几年实战中处理过的几个典型 DedeCMS 挂马案例,每个都对应了不同的攻击向量,合并起来能帮你建立更完整的防守认知。 案例一:UEditor 类型校验绕过。某地方门户站,攻击者把一个 PHP 后门通过 UEditor 上传接口传到 /uploads/allimg/202309/shell.php。原因是 UEditor 早期版本只校验了 Content-Type 头,没校验文件实际内容,攻击者用 Content-Type: image/jpeg 但 body 里塞 PHP 代码就绕过了。这个案例的关键启示是:不能把上传文件的安全完全交给应用层,nginx 层 deny PHP 是兜底防线。 案例二:plus/recommend.php SQL 注入升级。一个旧 DedeCMS 5.7 站点,攻击者利用 plus/recommend.php 的 SQL 注入漏洞读出管理员账号密码,登录后台后通过"模板管理 → 文件管理"上传 PHP 木马到 /data/cache/ 目录。如果当时已经禁了 /data/ 的 PHP 解析,即使后台被攻入,木马也跑不起来——攻击链就被切断了。 案例三:第三方插件后门。一个使用某"高仿 SEO 插件"的 DedeCMS 站点,插件本身就带了一个隐藏的 webshell 路由 /include/dedeseo/back.php。这种"自带后门"的插件防不胜防,禁 include 目录的 PHP 解析能直接挡住这类攻击。当然 include 目录确实存在被引用的合法 PHP 文件,但这些文件正常使用应该是被 require_once 引入而不是直接 URL 访问,所以禁直接访问没有副作用。 案例四:data 目录敏感文件泄露。一个站点没有禁 /data/ 目录,攻击者直接访问 data/sqldata/dede_admin-id.txt 读到了管理员密码哈希。这个案例展示了 deny 不仅能防止 PHP 执行,对敏感文件的读取保护同样重要。建议把 /data/ 整个目录直接 deny all(不管什么扩展名),写法是: location ^~ /data/ { deny all; return 403; } 这样连 .txt、.sql 备份文件都读不到。 ## 与 ModSecurity / Cloudflare WAF 的分工 有些同学已经接入了 WAF(云 WAF 或 ModSecurity),是不是就不用配本文的 nginx deny 规则了?答案是不行,二者是不同层次的防御: - WAF:基于流量特征做规则匹配,拦截已知攻击 payload。优点是覆盖广,劣点是规则一旦过时新攻击方式会绕过;且 WAF 默认信任来自服务器内部的请求,如果攻击者已经传了文件到服务器,访问该文件不会经过 WAF(特别是 ModSecurity 装在反代层时)。 - nginx 目录禁 PHP:基于路径/扩展名做静态规则,无论攻击 payload 多新颖,只要 PHP 文件落在被禁目录就跑不起来。 - fail2ban:基于 access.log 模式封 IP,对暴力探测有效但对单次攻击不及时。 - SELinux / AppArmor:操作系统级别的强制访问控制,PHP 进程即使想读 /etc/passwd 也读不到。这层最底,配置最复杂,多数中小站没启用。 四层叠加才是完整防御。本文的 nginx 配置是其中性价比最高的一层——配置简单、零运维成本、对正常业务零影响、却能挡住绝大多数自动化攻击。建议在 WAF 之外一定要加上。 ## 自动化巡检脚本 保哥写了一个简单的 bash 脚本,每天 cron 跑一次,自动检查 nginx 配置和 uploads 目录是否还安全: #!/bin/bash SITE_ROOT="/www/wwwroot/yoursite.com" NGINX_CONF="/www/server/panel/vhost/nginx/yoursite.com.conf" # 1. 检查 nginx 配置是否还包含 deny 规则 if ! grep -q "deny all" "$NGINX_CONF" || ! grep -q "uploads.*\.php" "$NGINX_CONF"; then echo "[ALERT] nginx deny 规则缺失" fi # 2. 扫描 uploads 目录是否有 PHP 文件 PHP_FILES=$(find "$SITE_ROOT/uploads" -name "*.php" 2>/dev/null) if [ -n "$PHP_FILES" ]; then echo "[ALERT] uploads 目录发现 PHP 文件:" echo "$PHP_FILES" fi # 3. 检查近 24 小时被修改的 PHP 文件 RECENT=$(find "$SITE_ROOT" -name "*.php" -mtime -1 2>/dev/null) if [ -n "$RECENT" ]; then echo "[INFO] 近 24 小时修改的 PHP 文件:" echo "$RECENT" fi 把这个脚本放到 /root/daily_check.sh,加 chmod +x,再 crontab -e 里加一行 0 6 * * * /root/daily_check.sh | mail -s "DedeCMS 安全巡检" you@example.com,每天早 6 点出一份巡检报告。任何异常都能在当天发现。 ## 常见问题解答 Q1:配置后图片上传不影响吧? 不影响。这条规则只针对 .php、.phtml 等扩展名,.jpg、.png、.gif、.webp 等正常图片格式不会被拦截。/uploads/ 目录依然可以正常存放和访问图片资源。 Q2:站点本来就靠 /uploads/preview.php 这种文件提供预览功能怎么办? 如果有合法的 PHP 入口在被屏蔽的目录下,需要单独放行。可以在 nginx 配置里加一条更具体的 location = /uploads/preview.php { ...正常 PHP 解析配置... },= 是精确匹配优先级最高,会绕过通用 deny all 规则。但更推荐的做法是把这种 PHP 文件迁出 uploads 目录,从架构上避免开洞。 Q3:宝塔面板的"防跨站攻击"开关跟这个有什么关系? 不一样。宝塔的"防跨站攻击"是 open_basedir 限制,让 PHP 进程不能跨站读其他站的文件。本文方案是 nginx 层不让某些目录的 PHP 被执行,二者是互补关系,建议都开启。 Q4:deny all 和 return 403 选哪个? 都行。deny all 是 ngx_http_access_module 的指令,最终也是返回 403。return 403 是 ngx_http_rewrite_module 的指令,更直白。两个一起写不冲突,是为了在某些边缘情况下双保险。如果只能写一个,return 403 更现代化。 Q5:会不会因为这个配置导致网站访问慢? 不会。nginx 的 location 正则匹配在编译期完成,匹配开销极小,平均每请求增加几微秒。比起被植入后门后清理代价低 10000 倍。 Q6:DedeCMS 已经停止维护了,这套加固还有意义吗? 有意义而且更重要。停止维护意味着不再有官方安全补丁,新发现的漏洞没人修,被动防守压力更大。这种情况下做好"目录禁 PHP + 文件权限收紧 + WAF + 监控"四道防线,是延长老站安全寿命的关键。如果业务允许,建议规划 12-24 个月内迁移到 WordPress、Typecho 或自研静态站。 Q7:把这套配置放在 nginx server 块还是 location 块? 放在 server { ... } 块内、其他 location 之前。nginx 的 location 匹配顺序是:精确匹配 = 优先 → ^~ 前缀匹配 → 正则匹配(按出现顺序) → 普通前缀匹配。本文的 ~* 正则匹配,写在 server 块内即可,不需要嵌套到其他 location 里。 这套配置部署完,DedeCMS 站点的安全水位会明显提升一档。下一篇保哥会继续讲怎么用 fail2ban + nginx 联动自动封禁恶意 IP,敬请期待。 ## Nginx反向代理实战指南:proxy_pass末尾斜杠、proxy_redirect、sub_filter、WebSocket与upstream全场景配置 - URL:https://zhangwenbao.com/nginx-proxy.html - 分类:Nginx - 发布:2020-12-22 | 更新:2026-05-16 - 摘要:Nginx反向代理远不止网传那几行proxy_pass。本文逐一拆解proxy_pass末尾斜杠带不带URI的四种拼接差异、proxy_redirect改写Location头、sub_filter替换响应体的三大限制,再覆盖WebSocket、SSE流式、大文件上传、upstream调度算法等生产场景。 - 关键词:反向代理,Nginx 配置,proxy_pass,WebSocket 代理,upstream > **TLDR**:摘要:Nginx反向代理远不止网传那几行proxy_pass。本文重点拆proxy_pass末尾斜杠带不带URI的关键差异、proxy_redirect处理后端发的301与302、sub_filter替换响应体绝对路径,再覆盖WebSocket、SSE流式与长轮询、大文件上传、upstream负载均衡、proxy_cache减少回源和HTTPS代理的特殊配置,附实战调试技巧。 > 摘要:Nginx反向代理远不止网传那几行proxy_pass。本文重点拆proxy_pass末尾斜杠带不带URI的关键差异、proxy_redirect处理后端发的301与302、sub_filter替换响应体绝对路径,再覆盖WebSocket、SSE流式与长轮询、大文件上传、upstream负载均衡、proxy_cache减少回源和HTTPS代理的特殊配置,附实战调试技巧。 Nginx 反向代理 (https://zhangwenbao.com/apache-proxy.html)是把内部服务藏在统一域名背后的核心手段,但实际配置里至少有十类常见场景:泛目录、子目录、整站、带斜杠与不带斜杠的 proxy_pass 行为差异、proxy_redirect 修正 location 头、sub_filter 替换响应体绝对路径、WebSocket 升级、流式输出、上传大文件、grpc 转发。本文按场景给出最小可工作的 nginx server 块配置,并讲清每条指令的边界条件——比如 proxy_pass 末尾斜杠的有无会影响整个请求路径的拼接逻辑,sub_filter 只对未压缩响应体生效等等。 ## 反向代理与正向代理的边界 先把概念厘清:正向代理面向客户端,客户端主动配置 proxy server 让它代为请求外网(公司内网用 squid 出网就是典型);反向代理面向服务器,客户端不知道实际服务在哪台机器,nginx 在前面伪装成统一入口(CDN、WAF、网关都是反向代理)。本文涉及的全部是反向代理。 反向代理的核心价值 (https://nginx.org/en/docs/http/ngx_http_proxy_module.html)是: - 统一入口:客户端只见到 example.com,背后可能是 Java 微服务、Python 服务、Go 网关多个进程并存。 - SSL 卸载:HTTPS 在 nginx 终结,后端走 HTTP 减少握手开销。 - 负载均衡:upstream 块加多台后端做轮询、加权、ip_hash。 - 缓存:proxy_cache 把热点内容缓存在 nginx 层。 - 请求改写:rewrite + proxy_pass 把 URL 形态映射到后端期望的形态。 ## proxy_pass 末尾斜杠的关键差异 这是 nginx 反向代理最容易翻车的地方,没有之一。 ## 规则简述 当 proxy_pass 后的 URL 带 URI(包括末尾斜杠 /),nginx 会把 location 匹配的部分从请求 URI 中替换掉;不带 URI 时,原始 URI 完整传递到后端。 ## 四种组合的实际行为 假设 location 是 /abc/,请求 /abc/foo/bar?x=1,对应四种 proxy_pass 写法的转发结果: proxy_pass 写法 | 转发到后端的 URI | proxy_pass http://backend; | /abc/foo/bar?x=1(完整传递) | proxy_pass http://backend/; | /foo/bar?x=1(去掉 /abc) | proxy_pass http://backend/api; | /api/foo/bar?x=1(替换 /abc 为 /api) | proxy_pass http://backend/api/; | /api/foo/bar?x=1(同上) | 注意:第三、第四种结果一样,但实现机制不同。第三种是 nginx 把 /abc/ 替换成 /api,然后拼上 /foo/bar,结果是 /api/foo/bar。第四种 nginx 把 /abc 替换成 /api/,再拼上 foo/bar,结果也是 /api/foo/bar。一致是因为运气好。 ## 规则失效的特殊场景 下面三种情况 proxy_pass 不能带 URI,nginx 会启动报错: - location 用了正则匹配(location ~ ^/api/) - location 内部用了 rewrite ... break - proxy_pass 后是变量(proxy_pass $upstream;) 这些场景下 nginx 没法做"替换 URI"动作,只能完整传递。如果你必须改写 URI,要在 location 里用 rewrite + proxy_pass 不带 URI 组合: location ~ ^/api/(.*)$ { rewrite ^/api/(.*)$ /v2/$1 break; proxy_pass http://backend; } ## 三种基础场景的最小配置 ## 整站反向代理 典型用途:example.com 反代到内部某台 IP 上的整站。 server { listen 80; server_name example.com; location / { proxy_pass http://192.168.1.10:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 四个 proxy_set_header 是生产标配: - Host:让后端拿到原始请求 Host,不然看到的是 nginx 的 IP。 - X-Real-IP:客户端真实 IP,用于日志、风控、地域识别。 - X-Forwarded-For:经过的代理链,多级代理时按逗号拼接。 - X-Forwarded-Proto:让后端知道客户端是 HTTPS 还是 HTTP,防止后端生成的 URL 协议错。 ## 子目录反向代理(带前缀) 典型用途:example.com/api/* 反代到独立 API 服务,example.com/* 走前端。 server { listen 80; server_name example.com; location /api/ { proxy_pass http://192.168.1.20:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { proxy_pass http://192.168.1.10:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } 注意 location /api/ 的 proxy_pass 末尾必须带 / —— 让 nginx 把 /api 前缀去掉再转发。如果不带 /,后端会收到 /api/users 而不是 /users,绝大多数后端会 404。 ## 泛目录(不带末尾斜杠的 location) 典型用途:example.com/abc 与 example.com/abc/foo 都转发到某后端。 server { listen 80; server_name example.com; location /abc { proxy_pass http://192.168.1.30/abc; } } 这种写法最大的坑是 example.com/abcd 也会命中 location /abc——nginx 的 location 前缀匹配不要求末尾斜杠对齐。如果要严格匹配 /abc/* 而不命中 /abcd,必须写 location /abc/。 ## proxy_redirect 处理后端发的 301/302 ## 问题描述 后端服务返回 302 跳转时,Location 头里带的是后端自己的 URL(比如 http://backend/login)。浏览器收到这个 Location 后会直接访问 backend,绕过 nginx。如果 backend 在内网,浏览器跳到内网 IP 立刻失败;如果 backend 在公网,跳过 nginx 等于绕过了 SSL 终结、WAF、统计。 ## 修复方案 用 proxy_redirect 把后端发的 Location 头改写成 nginx 路径: location /my/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_redirect http://backend/ http://$host/my/; } 这条 proxy_redirect 把 Location: http://backend/login 改写成 Location: http://example.com/my/login。三种语法形式: - proxy_redirect default; —— 默认行为,nginx 自动尝试做合理替换,对简单场景够用。 - proxy_redirect off; —— 完全不改写。 - proxy_redirect http://backend/ http://$host/my/; —— 显式指定改写规则。 ## HTTPS 终结场景的特别注意 如果 nginx 终结 HTTPS、后端是 HTTP,proxy_redirect 必须显式写: proxy_redirect http://backend/ https://$host/my/; 否则后端发的 http:// Location 头会让浏览器跳到 http 站,触发 mixed content 警告或跳转。 ## 相对路径的 Location 头 有些后端返回的 Location 是相对路径(Location: /login),这种情况 proxy_redirect 默认会处理:把 / 替换为 /my/。如果你显式写了规则但没覆盖相对路径,需要再加一行: proxy_redirect / /my/; ## 响应体内绝对路径的替换:sub_filter ## 问题:写死的 /public、/static、/api 很多老式 web 应用在模板里写死