Nginx的rewrite模块基于PCRE正则表达式,能够在请求处理阶段修改URI。很多配置文件里都能看到rewrite的身影,但真正理解它的执行顺序、flag作用域以及与location配合关系的人并不多。本文不会简单罗列参数,而是从实际配置里容易出错的地方切入,把语法、选择逻辑和避坑方法讲清楚。

一、rewrite指令基础与执行顺序
rewrite指令的基本语法为:rewrite regex replacement [flag];。其中regex是用于匹配请求URI的正则表达式,replacement是替换后的目标URI,flag用来控制重写后的处理方式。flag可以取last、break、redirect、permanent四种值,分别代表重新搜索location、停止重写并继续当前location、返回302临时重定向、返回301永久重定向。rewrite可以出现在server、location以及if上下文中,但在if中使用rewrite往往会带来难以排查的问题,后面会专门说明。
理解rewrite的执行顺序非常关键。Nginx在接收到请求后,会先执行server级别的rewrite规则,然后根据处理后的URI进行location匹配。匹配到location之后,再执行该location内部的rewrite规则。如果server级别或location内部的rewrite使用了last标志,Nginx会用新的URI重新发起location搜索,这意味着之前匹配到的location可能不再被使用。如果使用break标志,则不会重新搜索location,而是继续在当前location中执行后续指令。
此外,Nginx对重写循环有保护机制。如果rewrite规则反复修改URI,导致重写次数超过内部限制,通常会返回500错误。这种循环问题常见于正则范围过宽,例如把根路径也纳入重写条件,或者多个location之间的rewrite互相触发。排查循环问题时,开启重写日志能直观看到每一步URI的变化。
二、last与break的本质区别及选择方法
last和break是rewrite配置里最容易混淆的两个flag。它们的共同点是都会停止当前rewrite模块继续匹配后续的rewrite规则,但后续行为完全不同。last会让Nginx把重写后的URI当作新的请求URI,重新执行一遍location匹配过程;break则直接跳出rewrite模块,继续执行当前location中的其他指令,不会重新匹配location。简单说,last改变location上下文,break留在当前上下文。
用一个典型场景说明:假设有location /old/用于处理旧路径,并且存在另一个location /new/用于处理新路径。如果配置rewrite ^/old/(.*)$ /new/$1 last;,请求/old/page会跳转到/new/对应的location重新处理。而如果写成rewrite ^/old/(.*)$ /new/$1 break;,Nginx会在/old/这个location内继续处理/new/page,但不会去匹配/new/的location规则。此时如果/new/路径需要由特定location处理,break就会导致逻辑错误。
下表对比了四个flag的停止时机和后续行为:
| flag | 是否继续匹配后续rewrite | 是否重新搜索location | 客户端可见行为 |
|---|---|---|---|
| last | 否 | 是 | URI内部变化,地址栏不变 |
| break | 否 | 否 | URI内部变化,地址栏不变 |
| redirect | 否 | 否 | 返回302,地址栏变化 |
| permanent | 否 | 否 | 返回301,地址栏变化 |
选择last还是break,核心看是否需要重新匹配location。如果重写后的URI需要被另一个location块中的指令处理,例如重写到或静态文件或代理路径,应使用last。如果只是在当前location内修正URI,然后继续交给同一个location内的指令处理,比如配合proxy_pass做路径裁剪,应使用break。对于简单的http跳转,一般优先考虑return指令,因为return直接终止请求处理,比rewrite加redirect或permanent更高效。
三、典型应用场景与配置示例
伪静态是rewrite最常见的用途之一。例如访问/article/123.html时,实际交给/article.php?id=123处理,可以这样配置:
location /article/ {
rewrite ^/article/(\d+)\.html$ /article.php?id=$1 last;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
}
}
这里正则中的反斜杠用于转义点号,确保只匹配真正的.html后缀,而不是任意字符加html。last标志使重写后的/article.php?id=123重新匹配到.php的location进行解析。如果漏掉了锚点$,可能会把/article/123.htmlabc也错误匹配进来,所以要养成使用^和$限定完整范围的习惯。
强制HTTPS跳转也是高频需求。下面用return替代rewrite完成301跳转:
server {
listen 80;
server_name ipipp.com;
return 301 https://$host$request_uri;
}
这段配置放在80端口server块中,对任何请求直接返回301,不需要使用if判断scheme,也不依赖rewrite。这样配置更简洁,性能也更好。对于需要保留原始URI的跳转,使用$host$request_uri变量即可。不要在location中用if加rewrite做同样的跳转,因为if在Nginx中的行为并不直观,容易产生意料之外的匹配。
反向代理场景中,rewrite经常用于剥离路径前缀。假设外部请求/api/user/list需要转发到后端的/user/list,可以这样写:
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend;
}
这里使用break是因为只需要在当前location内完成URI裁剪,然后让proxy_pass把重写后的URI发送给后端。注意proxy_pass后面没有带URI路径,所以Nginx会把重写后的完整URI传给上游。如果写成proxy_pass http://backend/;带了斜杠,就会替换掉location匹配的前缀部分,可能影响转发结果。这类细节在配置时务必结合变量和访问日志验证。
四、注意事项与避坑建议
第一条避坑建议是尽量减少在if中使用rewrite。Nginx官方文档明确说明if指令在location内使用时会产生问题,因为if会创建隐式的配置上下文,可能导致请求处理分离。例如下面这段看似正常的配置,实际可能让某些请求无法按预期处理:
location / {
if ($http_user_agent ~* "MSIE") {
rewrite ^(.*)$ /msie/$1 break;
}
# 其他指令
}
更推荐的做法是用map指令预先定义变量,再通过变量选择处理路径,或者使用try_files来避免if。如果确实需要条件判断,可以在server级别使用if配合return做跳转,但也要小心配置冲突。对于绝大多数路径调整需求,优先考虑location匹配本身,而不是在if中重复判断。
正则性能也是需要关注的环节。rewrite中使用的正则表达式每次请求都会执行,过于宽泛的模式比如^/(.*)看似通用,但捕获组会带来额外开销。如果只是简单的前缀替换,尽量使用location的精确匹配或前缀匹配,再配合较窄的正则。捕获组按需使用,不必为了统一写法而把所有内容都塞进一个大的重写规则。压缩不必要的rewrite规则,能有效减少CPU消耗。
调试rewrite时,可以临时打开重写日志:
server {
error_log /var/log/nginx/rewrite.log notice;
rewrite_log on;
}
设置rewrite_log on后,Nginx会把每一步重写的结果写入error_log指定的日志文件。通过观察日志,能清楚看到URI从初始值经过哪些规则变成了最终值,以及在哪个环节发生了循环。排查完毕后记得关闭rewrite_log,避免线上日志体积膨胀。遇到500错误时,优先查看重写日志确认是否存在超过10次的循环重写。
最后要提醒的是,rewrite不会改变客户端地址栏中的URL,只有redirect和permanent会返回3xx让浏览器发起新请求。如果需要SEO友好的永久跳转,应该使用301;如果只是临时维护或跳转,使用302。不要用rewrite加permanent来实现复杂跳转,因为一旦规则写错,浏览器会长期缓存旧的301结果,用户很难立即看到修正后的效果。简单跳转优先return,复杂路径改写真得需要rewrite时,再结合last或break精确定义边界。
Nginx rewriterewrite配置URL重写修改时间:2026-10-05 17:23:27