location 匹配规则与常见坑
「配置明明写了,为什么没生效」——Nginx 里九成的这类问题都出在 location 的匹配规则上。四类修饰符有自己的优先级,正则按书写顺序短路,^~ 只在它是最长前缀时才起作用,root 与 alias 在 location 里的拼接方式还完全不同。本章把匹配算法拆开讲,并给一份排查清单。
要解决的问题
- 同一路径写了多个 location,到底哪一个生效?
location /static/与location ~ \.png$同时存在,图片请求走哪个?- 为什么
alias明明指向了正确目录,还是 404 或多了一层目录? - 正则里的捕获组(
$1)怎么用进proxy_pass和rewrite? try_files与 location 怎么配合,SPA 回退和 PHP 安全防护怎么写才不出错?rewrite和return到底该选哪个?
匹配优先级
Nginx 的匹配不是「按书写顺序从上到下」,而是分两轮:
| 顺序 | 修饰符 | 形式 | 规则 |
|---|---|---|---|
| ① | = | location = /healthz | 精确匹配,命中即结束,不再看任何其他规则 |
| ② | ^~ | location ^~ /static/ | 前缀匹配,且必须是最长前缀才生效;命中后跳过正则 |
| ③ | ~ / ~* | location ~* \.(png|jpg)$ | 正则匹配,按配置文件出现顺序逐个试,第一个命中即用 |
| ④ | 无 | location /api/ | 普通前缀,记录最长匹配,先留着不立即生效 |
| ⑤ | — | location / | 兜底 |
完整算法是:先找 = 精确匹配;再找出最长的普通前缀匹配,如果它带 ^~ 就直接用它;否则按顺序试正则,命中就用正则;正则全不命中,才用刚才记住的最长前缀。所以 ^~ 不是「比正则优先」的通用规则,只有它同时是最长前缀时才优先。
root 与 alias 在 location 里的差异
| 维度 | root | alias |
|---|---|---|
| 拼接方式 | root 值 + 完整 URI | alias 值替换掉 location 前缀 |
| 尾斜杠 | 不敏感,但别写尾斜杠 | 必须与 location 前缀一致,否则路径错层 |
| 能否用在正则 location | 可以 | 可以,且必须配捕获组才能定位到文件 |
与 try_files 配合 | 行为稳定 | 历史上存在怪异行为,优先用 root |
# root:路径 = /srv/www + 完整 URI
location /assets/ { root /srv/www; } # /assets/a.png → /srv/www/assets/a.png
# alias:location 前缀被替换,两边斜杠必须一致
location /assets/ { alias /srv/media/; } # 正确 → /srv/media/a.png
location /assets/ { alias /srv/media; } # 错误 → /srv/mediaa.png
# 正则 location 里的 alias:用捕获组拼出文件名
location ~* ^/img/(.+\.(?:png|jpe?g|webp))$ { alias /srv/media/$1; }
正则捕获组与 rewrite
# 1) 正则 location + return:把 $1 拼进新地址
location ~ ^/old/(.*)$ { return 301 /new/$1; }
# 2) 正则 location + proxy_pass:直接写 URI 会报错,用变量才合法
location ~ ^/api/v(\d+)/(.*)$ {
proxy_pass http://127.0.0.1:8080/v$1/$2; # 含变量时,URI 整体替换原始请求路径
}
location ~ ^/api/ {
proxy_pass http://127.0.0.1:8080; # 不带 URI:路径原样透传,最省心
}
# 3) rewrite:改完路径后重新走 location 匹配(去掉 break 时)
location /blog/ { rewrite ^/blog/(.*)$ /articles/$1 last; }
location /articles/ { proxy_pass http://127.0.0.1:8080; }
rewrite 的四种 flag 要分清:
| flag | 作用 | 是否重新匹配 location |
|---|---|---|
last | 用新 URI 重新走一遍 location 匹配 | 是 |
break | 停止 rewrite,继续在当前 location 内处理 | 否 |
redirect | 返回 302 临时跳转 | 否(客户端重新请求) |
permanent | 返回 301 永久跳转 | 否 |
permanent 会被浏览器长期缓存,改错了用户几个月都跳不回来,临时调整一律用 redirect。
try_files 与 location 的分工
server {
root /srv/www/site;
location / { try_files $uri $uri/ /index.html; } # SPA 回退,前端路由接管
location /api/ { proxy_pass http://app_backend; } # 接口走反代,不被 / 吃掉
location ~ \.php$ { try_files $uri =404; } # 关键:文件不存在直接 404
location /files/ { try_files $uri $uri/ =404; } # 纯静态目录
}
location ~ \.php$ { try_files $uri =404; } 是必须写的:没有它,任何 /uploads/evil.php 这类不存在的路径都会被交给解释器执行,属于典型的安全漏洞。try_files 里的 /index.html 前面有 / 表示内部重定向,会重新走 location /index.html 的规则,所以给 HTML 加的 no-cache 头依然生效。
「location 没生效」排查清单
# 1) 看合并后的最终配置(含所有 include),确认哪条真的生效了
nginx -T | grep -n 'location' -A6
# 2) 确认请求落在哪个 server 块(server_name 不匹配会落到 default_server)
nginx -T | grep -n 'server_name'
# 3) 打开 debug 日志看匹配过程(需 debug 版本或 error_log debug)
sudo tail -100 /var/log/nginx/error.log
# 4) 用带查询串的方式绕开浏览器缓存做实测
curl -sI 'https://example.com/static/a.png?v=1' | head -3
按概率排序的原因:
| 现象 | 常见原因 | 确认方法 |
|---|---|---|
| 完全不生效 | 配置文件没被 include;改的是 sites-available 而生效的是 sites-enabled 软链 | nginx -T 里搜不搜得到这段 |
配了两个 location / | nginx -t 报 duplicate location | 校验输出直接看 |
| 正则 location 抢走了请求 | 正则按顺序短路,先写的先赢 | nginx -T 看书写顺序 |
^~ 没起作用 | 它旁边有更长的普通前缀 | 把两条前缀长度对一遍 |
| 落到了别的站点 | server_name 没匹配上,用了默认 server | nginx -T 看 default_server |
| 改了但没生效 | 只改了文件没 reload | nginx -t && systemctl reload nginx |
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
| 以为书写顺序决定优先级 | 精确匹配反而被写在后面,仍然生效 | 记住「精确 > ^~ 最长前缀 > 正则顺序 > 最长普通前缀 > /」 |
alias 与 location 尾斜杠不一致 | 404 或多一层目录 | 两边要么都带 /,要么都不带 |
SPA 回退写在 location /,接口被吃掉 | 接口返回 HTML | /api/ 单独 location,且写在前面 |
rewrite ... permanent 用于临时跳转 | 改不回来,用户被缓存锁死 | 临时跳转用 redirect |
try_files 少了 =404 | 不存在的脚本被交给解释器 | 动态脚本 location 必加 try_files $uri =404; |
| 改了配置没校验 | 语法错误只有 reload 时才暴露 | 固定 nginx -t && systemctl reload nginx |
小结:location 的优先级是「精确匹配 > 最长的 ^~ 前缀 > 按顺序的第一个正则 > 最长的普通前缀 > /」,^~ 只有在它同时是最长前缀时才跳过正则;root 是拼接完整 URI、alias 是替换 location 前缀,尾斜杠必须两边一致;路径类问题不要去猜,用 nginx -T 看合并后的真实配置、用 curl -I 看真实结果,两分钟就能定位。