Nginx location 匹配规则与 rewrite 重写实战:从入门到排错

Nginx 的配置里,location 和 rewrite 是出现频率最高的两个指令,也是新手最容易搞混的部分。很多站长遇到过这种情况:明明照着教程写了跳转规则,结果要么跳转不生效,要么把整个站都跳错了;明明写了 location 规则,请求却总是走到默认的那一段。这篇文章把 location 的匹配规则和 rewrite 重写逻辑从头到尾讲透,配上实战案例,看完基本就能自己排掉配置里的坑。

一、location 的语法与四种匹配类型

location 指令的完整语法是 location [修饰符] 匹配路径 { ... },修饰符决定了匹配方式,一共分四种。

第一种是精确匹配,修饰符是等号 =,比如 location = /favicon.ico { ... }。精确匹配要求请求的 URI 和后面写的路径完全一致才命中,一个字符都不能差。它适合放那些路径固定、访问频繁的资源,比如站点图标、robots.txt、sitemap.xml,命中后 Nginx 不再做任何其他匹配,直接进入这个块处理。

第二种是前缀匹配,也就是不带任何修饰符,比如 location /images/ { ... }。它匹配以指定路径开头的所有 URI,只要请求路径以 /images/ 开头就算命中。前缀匹配遵循最长匹配原则:同时有多个前缀匹配规则时,匹配路径最长的那条生效。比如 /images/ 和 /images/logos/ 两条规则同时存在,请求 /images/logos/logo.png 时,后面那条更长,所以由它处理。

第三种是正则匹配,修饰符是波浪号,区分大小写用 ~,忽略大小写用 ~*。比如 location ~* \.(jpg|png|gif)$ { ... } 可以匹配所有图片文件,不管扩展名是大写还是小写。正则匹配按配置文件中出现的顺序执行,从上到下,第一个匹配成功的就生效,后面的不再检查。

第四种是 ^~ 修饰的前缀匹配,比如 location ^~ /static/ { ... }。它的特殊之处在于:如果这个前缀匹配命中,就直接使用它,不再继续检查正则表达式。这相当于给某些前缀路径开了"免检通道",防止它们被后面的正则规则抢走。

二、匹配优先级:一个请求到底走哪条规则

搞清楚四种类型之后,最关键的问题是优先级。Nginx 处理一个请求时,匹配顺序是这样的:第一步,先做前缀匹配,找出所有命中的前缀规则,如果存在精确匹配(=),直接使用,流程结束;第二步,如果没有精确匹配,比较所有前缀匹配,选出最长的那条;第三步,如果这条最长的前缀匹配带有 ^~ 修饰符,直接使用它,流程结束;第四步,如果最长的前缀匹配不带 ^~,Nginx 会把这条规则先记下来,然后继续按顺序检查所有正则规则,一旦某个正则命中,就用正则的结果;第五步,如果所有正则都没命中,才使用刚才记住的那条最长前缀匹配。

把这个流程记住,绝大多数 location 配置问题都能自己分析。举个例子:

location / { ... }
location = / { ... }
location /images/ { ... }
location ^~ /static/ { ... }
location ~* \.(gif|jpg|png)$ { ... }

请求 / 时,精确匹配 location = / 命中,直接用它;请求 /images/logo.jpg 时,前缀匹配最长的是 /images/,但后面正则 ~* \.(gif|jpg|png)$ 也命中,因为 /images/ 没有 ^~ 修饰符,所以最终由正则规则处理;请求 /static/js/app.js 时,^~ 前缀命中,跳过正则,由 /static/ 规则处理;请求 /about.html 时,没有精确匹配,最长前缀是 /(location / 匹配一切),正则里没有命中 .html 的规则,所以走 location / 的默认处理。

新手最常见的误区有两个。第一个是把正则规则放在前缀规则前面就觉得优先级高,其实正则只在前缀匹配"最长项不带 ^~"的前提下才参与,而且优先级逻辑里精确匹配永远第一。第二个误区是以为 location 的顺序决定优先级,实际上对前缀匹配来说顺序无关紧要,只有正则匹配才跟顺序有关。记住"精确 > ^~ > 正则 > 普通前缀"这个口诀,比死记文档管用。

三、rewrite 基础:语法与四个标志

rewrite 指令的语法是 rewrite 正则表达式 替换内容 [标志];。它做的是 URL 重写,把请求的 URI 按正则匹配后替换成新的内容。rewrite 可以写在 server 块里,也可以写在 location 块里,作用范围不同:server 块里的 rewrite 对所有请求生效,location 块里的只对进入该 location 的请求生效。

rewrite 有四个标志,含义完全不同,这是最容易踩坑的地方。

last:停止处理当前这条 rewrite 规则,但会用替换后的 URI 重新发起一轮 location 匹配。也就是说,rewrite 之后请求会重新走一遍 location 匹配流程,适合在 server 块里改写路径后交给对应 location 处理。

break:停止处理当前这条 rewrite 规则,而且不再重新匹配 location,直接用替换后的 URI 继续当前 location 块里的后续指令。区别在于 last 会"重新走流程",break 不会。

redirect:返回 302 临时重定向,让浏览器地址栏变成新地址。适合临时性的跳转,比如网站维护期间的跳转。

permanent:返回 301 永久重定向,浏览器和搜索引擎都会记住这个新地址。域名变更、页面永久移动时用这个,对 SEO 来说 301 会把旧地址的权重传递到新地址。

四、rewrite 实战:六个高频场景

场景一,HTTP 强制跳转 HTTPS。这是最常用的规则,写在 80 端口的 server 块里:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

注意这里用的是 return 而不是 rewrite。对于纯跳转需求,return 比 rewrite 更高效,因为它直接结束请求,不再做正则处理。能用 return 解决的问题就不要用 rewrite,这是 Nginx 官方也推荐的做法。

场景二,主域名统一。把 www 域名 301 到不带 www 的域名,或者反过来,看站长的偏好:

server {
    listen 443 ssl;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

这里有个细节:$request_uri 是原始请求的完整路径和参数,包括查询字符串,用它可以保证跳转后不丢参数。如果写成 $uri,查询字符串可能会丢。

场景三,伪静态。Typecho、WordPress 这类程序开启伪静态后,需要把动态地址重写成入口文件:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

try_files 的逻辑是:先尝试把请求当作真实文件,$uri 存在就直接返回;再尝试目录;都不存在就把请求交给 /index.php 处理,参数原样传递。这是现代 PHP 程序最标准的伪静态写法,比一堆 rewrite 规则更清晰。注意 rewrite 在伪静态场景下还有一个经典坑:如果 PHP 程序自己又返回了重定向,Nginx 的 rewrite 可能会和程序的重定向循环叠加,出现无限 301,遇到这种情况要检查程序配置的站点地址和实际地址是否一致。

场景四,旧路径迁移。网站改版后旧页面路径变了,需要把旧路径重写到新路径:

location /old/ {
    rewrite ^/old/(.*)$ /new/$1 permanent;
}

这条规则把 /old/ 下的所有路径永久重写到 /new/ 下,$1 捕获的是旧路径去掉 /old/ 前缀后的部分。用 permanent 而不是 last,是为了让搜索引擎把权重转移到新地址,同时用户访问旧链接也能被正确带到新页面。

场景五,移动端跳转。根据 UA 判断是否移动设备,跳转到 m 站:

if ($http_user_agent ~* "(android|iphone|ipad)") {
    rewrite ^(.*)$ https://m.example.com$1 permanent;
}

这里用到了 if 指令。需要提醒的是:if 在 location 块里有很多限制,官方文档明确说 if 在 location 上下文里是邪恶的(evil),容易产生难以预料的行为。能不用 if 就不用,实在要用尽量放在 server 块里,或者改用 map 指令在更早的阶段完成判断。

场景六,带参数的跳转。有些场景跳转时要保留参数,比如分页参数:

location = /list {
    rewrite ^ /list/page/1/?$args permanent;
}

$args 是查询字符串变量,这样 /list?page=2 会跳到 /list/page/1/?page=2。这类带变量的写法在写跳转规则时非常实用。

五、location 与 rewrite 配合的坑

第一个坑是 proxy_pass 的路径拼接。location 使用正则或者带路径的前缀时,proxy_pass 后面带不带 URI 会直接影响转发结果。比如 location /api/ { proxy_pass http://backend; } 会把完整的 /api/xxx 转发过去;而 proxy_pass http://backend/; 末尾带斜杠时,location 匹配到的部分会被替换掉,转发出去的路径会变成 /xxx。这个细微差别经常导致后端接口 404,排查时要先看 Nginx 到底把什么路径转发给了后端,在日志里加 $upstream_addr 和 $request_uri 一目了然。

第二个坑是 rewrite 与 location 的循环。last 标志会重新触发 location 匹配,如果 rewrite 后的 URI 又匹配到同一条 rewrite 规则,就会无限循环。Nginx 默认最多循环 10 次,超过会返回 500 错误。出现 500 且错误日志里有 rewrite or internal redirection cycle 字样,就是踩了这个坑。解决方法是把 rewrite 规则写得更精确,或者改用 break 标志。

第三个坑是 try_files 与 rewrite 的互相干扰。有些配置在 location 里既写了 rewrite 又写了 try_files,执行顺序混乱,导致图片请求全部走到 PHP。原则是:能用 try_files 解决的静态文件判断就不要写 rewrite,两者职责分开,配置会清晰很多。

第四个坑是 location 里写 if 判断文件是否存在:if (!-f $request_filename) { rewrite ...; } 这种写法在旧教程里非常流行,但在 if 里做文件判断是官方明确不建议的,行为不可预测,应该用 try_files 替代。

六、调试与验证技巧

改完配置先做语法检查:

nginx -t

语法通过后重载配置:

nginx -s reload

然后验证跳转是否生效,用 curl 看响应头:

# 查看是否 301 以及 Location 指向哪里
curl -I http://example.com/old/page
# 带 UA 测试移动端跳转
curl -I -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" https://example.com/

如果跳转没生效,先确认请求真的到达了这台服务器,再用 access log 确认命中规则。Nginx 的 access log 默认不记录 rewrite 过程,可以在 error log 里临时打开 debug 级别观察匹配过程,或者用 add_header X-Debug $uri 在响应头里输出当前 URI,快速定位是哪条规则在处理。

另外推荐一个习惯:每个 location 块里的关键配置都加注释,写明这条规则是干什么的。Nginx 配置不换机器还好,一换机器、一交接,没有注释的规则就是天书。我自己吃过这个亏,后来所有规则都写清楚来源和用途,排错效率高了很多。

七、常见问题 FAQ

问:rewrite 和 return 怎么选? 纯跳转用 return,简单高效;需要正则改写路径再继续处理用 rewrite;需要保留查询参数重定向到新地址,两者都能实现,return 写法更简洁。

问:301 和 302 有什么区别,什么时候用哪个? 301 是永久重定向,搜索引擎会更新索引并把权重转移给新地址,适合域名变更、永久改版;302 是临时重定向,搜索引擎会保留原地址,适合活动页、临时维护页。用错 301 会让搜索引擎把权重转移到临时地址,事后想改回来很麻烦。

问:为什么我的正则 location 不生效? 先检查前缀匹配里有没有带 ^~ 的规则抢先命中,再检查正则写法和顺序。正则规则按顺序从上到下执行,第一条命中的生效,所以通用规则要放在具体规则后面。

问:重写后出现无限循环 500 怎么办? 看错误日志里有没有 rewrite or internal redirection cycle,找到循环的那条规则,把 last 改成 break,或者让替换后的 URI 不再匹配原规则。也可以在规则里加条件限制,让同一 URI 只被处理一次。

location 和 rewrite 是 Nginx 里最值得花时间啃透的部分。把匹配优先级和四个标志的语义弄明白,再配合 curl 和日志做验证,配置起来就心中有数了。希望这篇实战总结能帮你在配置 Nginx 的时候少走弯路。

Last modification:August 8th, 2026 at 08:20 am

Leave a Comment