一篇写给被 WordPress 伪静态和 Nginx 规则绕晕的站长。看完你会知道:为什么首页能打开、图片能缓存、后台会跳转、部分文件会 404,而爬虫还在偷偷爬你的文章页。
一、先记住一句话:location 匹配只看“路径”,不看问号后面
很多人卡在第一步,是因为把 URL 当成一整串。Nginx 不这么看。
https://example.com/?s=wordpress 在 Nginx 眼里拆成两半:
- path(路径):
/ ← location 只匹配这个 - query string(查询串):
s=wordpress ← location 完全不看
所以「搜索页」/?s=xxx 在 location 眼里就是一次对 / 的请求。理解这一点,后面所有匹配就都顺了。
二、location 的优先级:从高到低四层
Nginx 匹配 location 有明确的优先级,不是按书写顺序从上往下找:
location = /path 精确匹配 —— 最高优先级,命中即停location ^~ /path 前缀匹配且停止正则 —— 本篇配置没用到location ~ regex / ~* regex 正则匹配 ——
按出现顺序,第一个命中的生效,命中即停location /path 普通前缀匹配 —— 最长前缀优先location / 兜底
一句话记忆:精确 > 正则(按顺序先到先得)> 前缀 > 兜底 /。
location ~ [^/]\.php(/|$) { try_files $uri =404; fastcgi_pass unix:/tmp/php-cgi-84.sock; fastcgi_index index.php; include fastcgi.conf; include pathinfo.conf; }location /{ try_files $uri $uri/ /index.php?$args;}rewrite /wp-admin$ $scheme://$host$uri/ permanent; location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ { return 404; } location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ { return 404; } location ~ \.well-known{ allow all; } if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { return 403; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; error_log /dev/null; access_log /dev/null; } location ~ .*\.(js|css)?$ { expires 12h; error_log /dev/null; access_log /dev/null; }
我们这份 WordPress 配置里,我把include文件引入的内容直接也贴出来了,include 进来的 PHP 正则在最顶部,所以它是所有正则里第一个被尝试的——这个位置很关键,后面会反复用到。
三、把这份配置的 location 按“实际优先级”重排
配置书写顺序不等于生效顺序。按 Nginx 规则重排后是这样:
| 实际优先级 | location | 作用 |
|---|
| 1(精确) | = /xmlrpc.php | 直接 403,挡 XML-RPC 攻击 |
| 1(精确) | = /tongji/gif.gif | 内部 rewrite 到 gif.php(若有) |
| 2(正则,第一个) | `~ [^/].php(/ | $)` |
| 3 | ~* 敏感文件 | 备份/SQL/配置等返回 404 |
| 4 | ~* 敏感目录 | .git/node_modules 等返回 404 |
| 5 | ~ .well-known | SSL 证书验证放行 |
| 6 | ~ .*\.(gif|jpg|...) | 图片静态缓存 30 天 |
| 7 | ~ .*\.(js|css)?$ | JS/CSS 静态缓存 12 小时 |
| 8(兜底) |
location / | try_files → /index.php |
注意两件事:
rewrite /wp-admin$ ... permanent; 写在 server 块里,不属于 location,它在 location 匹配之前、server 级 rewrite 阶段就把 /wp-admin 301 跳转了。- PHP 正则因为来自最顶部的,抢在所有其它正则前面接管
.php 请求。
四、逐个页面走一遍:WordPress 常见请求到底命中谁
1. 首页 /
- 不是精确匹配,不含
.php,不命中任何敏感/静态正则 - 落到
location /:try_files $uri $uri/ /index.php?$args / 是目录,$uri/ 命中 → 触发目录 index → index.php
- 进入 PHP 正则 → PHP-FPM → WordPress 输出首页
2. 文章页 /hello-world/、分类页 /category/news/、标签页、作者页
这是 WordPress 伪静态的核心场景。
- 磁盘上没有
hello-world 这个文件/目录 location / 里 try_files $uri $uri/ /index.php?$args:前两个都找不到 → 内部改写成 /index.php?$args- 改写后 Nginx 重新做一次 location 匹配 → 这次命中最顶部的 PHP 正则 → PHP-FPM
- WordPress 解析 URI → 返回对应文章
划重点:伪静态不是“把 URL 变文件”,而是“找不到文件就交给 index.php 路由”。 所有文章/分类/标签/作者/日期归档,全靠这一条 try_files 兜底。
3. 搜索页 /?s=wordpress
- path 是
/,query 是 s=wordpress,location 不看 query - 所以它和首页一样走
location / →
try_files → index.php $args 把 s=wordpress 原样带给 PHP,WordPress 才拿到搜索词
顺带戳破一个常见误解:很多人以为配置里写了含 search 的限速正则就能限住搜索页。标准 ?s= 搜索的 path 里根本没有 search 字样,限不到。 只有伪静态成 /search/xxx/ 的搜索才会命中。
4. 后台 /wp-admin(不带斜杠)
- 在 location 匹配之前,
server 块的 rewrite /wp-admin$ ... permanent 先触发 - 301 永久跳转到
/wp-admin/ - 浏览器地址栏变成带斜杠,后台相对资源才能正确加载
带斜杠的 /wp-admin/ 再走 location / → try_files → index.php
→ PHP 处理后台入口。
5. 图片 /wp-content/uploads/2024/01/photo.jpg
- 依次试正则,PHP 正则不匹配(不是
.php) - 命中
~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ → 命中即停 - Nginx 直接返回文件,
expires 30d 浏览器缓存 30 天 - 不会走
location /,也不会惊动 PHP
这里有个细节:图片 location 没有 try_files。所以图片存在就正常返回;图片不存在,Nginx 直接给自己的 404,不会回落到 WordPress 的 404 页面。这是正常的——静态资源本就该由 Nginx 管。
6. CSS / JS /wp-content/themes/x/style.css
- 不命中 PHP、不命中图片
- 命中
~ .*\.(js|css)?$ → 静态返回,缓存 12 小时 - 同样不走
location /
7. 真实存在的 PHP /index.php、/wp-admin/admin-ajax.php
- 最顶部的 PHP 正则最先命中(它是第一个正则)
- 进 PHP location 后先
try_files $uri =404:文件存在 → 通过 fastcgi_pass 交给 PHP-FPM 执行
8. 不存在的 PHP /evil.php(扫描器探测)
- 命中 PHP 正则
try_files $uri =404:磁盘上没有 evil.php → 直接 404- 不会被丢给 PHP-FPM 执行
这条 =404 是安全护栏,防止“任意 .php 都送去 FPM”导致的信息泄露。
9. 敏感文件 /.env、/backup.sql、/composer.json
- 命中
~* 敏感文件 正则 → 返回 404(伪装成不存在,不泄露“这里有东西”)
10. 敏感目录 /.git/config、/node_modules/x.js
11. SSL 证书验证 /.well-known/acme-challenge/xxx
- 命中
~ .well-known → allow all - 但若该目录里被塞了
.php/.js/.zip 等敏感后缀,后面的 if 判断 → 403
五、一张图看懂匹配流程
请求进来
│
├─ server 级 rewrite:/wp-admin$ → 301 /wp-admin/
│
├─ 精确匹配 = ? ──是──> 403 或内部 rewrite(xmlrpc、tongji)
│
├─ 第一个正则:~ [^/]\.php(/|$) ? ──是──> try_files=404 → PHP-FPM
│
├─ ~* 敏感文件 ? ──是──> 404
├─ ~* 敏感目录 ? ──是──> 404
├─ ~ .well-known ? ──是──> 放行(敏感后缀 403)
├─ ~ 图片后缀 ? ──是──> 静态 30d 缓存
├─ ~ js|css ? ──是──> 静态 12h 缓存
│
└─ location / ──> try_files $uri $uri/ /index.php?$args
│
├─ 文件/目录存在 ──> 静态或 index
└─ 不存在 ──> 内部改写 /index.php
└─ 重新匹配 → 命中 PHP 正则 → FPM
六、这份配置里的几个“坑”,知道了能少踩
坑 1:.js|css 正则的 ?
~ .*\.(js|css)?$ 里的 ? 让分组变成“可选”,实际语义被放大,可能把某些含点号的伪静态页面误判成静态资源。规范写法是 ~ \.*\.(js|css)$。
坑 2:含 search 的限速正则拦不到 ?s=
前面说过,query 不参与 location 匹配。想限速搜索/翻页/爬取,更靠谱的是把 limit_req 直接写进 location /,覆盖所有动态页面,而不是靠一条只匹配 path 子串的正则。
坑 3:静态资源 location 没有 try_files
图片/CSS/JS 不存在时是 Nginx 自己的 404,不会走 WordPress 主题的 404 页。排查“图片 404 怎么不显示主题 404”时要知道这点。
坑 4:PHP 正则在最顶部,抢在所有正则前
include enable-php-84.conf 放在文件开头,意味着 .php 请求优先被 PHP 正则接管。只有 location = 精确匹配能压过它(比如 xmlrpc.php 的 403)。
七、回到这个问题:怎么限速防爬虫
把上面的匹配逻辑串起来,结论很清楚:
- 静态资源(图片/CSS/JS)走各自正则,根本不经过
location /,所以写在 location / 的限速天然管不到静态资源——你不用担心采集器抓图片被限速,也不用为静态资源单独操心。 - 所有动态页面(首页/文章/分类/搜索/分页/归档)都通过
try_files 落到
index.php,它们才是爬虫走的重灾区。 - 所以想限速“访问 WordPress 页面的 IP”,最贴合需求的做法就是把
limit_req 写进 location /,一条覆盖全部动态页面,再配合 map 给 /wp-admin/、wp-login.php 置空 key 放行后台即可。
八、三句话记住 location
- location 只看 path,不看
? 后面的 query。 - 优先级:精确
= > 正则 ~(按顺序先到先得)> 前缀 > 兜底 /;正则一旦命中就停止。 - WordPress 伪静态的本质:磁盘找不到文件 →
try_files 兜底到 /index.php → 重新匹配命中 PHP 正则 → 交给 WordPress 路由。
搞懂这三条,再复杂的 Nginx 配置你也能一层层拆开看。