Nginx rewrite 指令详解
你已经把基本概念总结得很清楚了,我来补充一些更深入的理解、实际应用场景以及注意事项,帮你更全面地掌握 rewrite 指令。
一、rewrite 指令完整语法
rewrite 正则表达式 替换内容 [flag];
执行流程:
- 检查请求 URI 是否匹配正则表达式
- 如果匹配,将 URI 替换为新的内容
- 根据 flag 决定后续行为
二、flag 标记的深入理解
1. last 和 break 的区别(重点)
| 维度 | last | break |
|---|---|---|
| 是否重新匹配 location | ✅ 会 | ❌ 不会 |
| 执行位置 | 完成 rewrite 后,重新搜索 location | 停止 rewrite,继续执行当前 location |
| 适用场景 | rewrite 后需要进入新的 location | rewrite 后还在当前 location 处理 |
| 性能 | 稍低(需重新匹配) | 更高 |
实例对比:
server {
listen 80;
server_name example.com;
location /test/ {
rewrite ^/test/1.html /test/2.html last;
rewrite ^/test/2.html /test/3.html break;
rewrite ^/test/3.html /test/4.html last;
add_header X-Rewrite-Status "executed";
return 200 "最终 URI: $uri";
}
location /test/2.html {
return 200 "这是 location /test/2.html";
}
}
请求 /test/1.html 的执行流程:
1. 匹配 ^/test/1.html → 重写为 /test/2.html
2. last 标志 → 重新搜索 location
3. 进入 location /test/2.html
4. 返回 "这是 location /test/2.html"
请求 /test/2.html 的执行流程:
1. 匹配 ^/test/2.html → 重写为 /test/3.html
2. break 标志 → 不重新搜索 location
3. 继续执行当前 location
4. 添加 X-Rewrite-Status 头
5. 返回 "最终 URI: /test/3.html"
2. redirect 和 permanent 的区别
| 维度 | redirect | permanent |
|---|---|---|
| HTTP 状态码 | 302 | 301 |
| 含义 | 临时重定向 | 永久重定向 |
| 浏览器缓存 | 不缓存 | 会缓存 |
| SEO 影响 | 权重不转移 | 权重转移到新 URL |
| 适用场景 | 临时维护、A/B测试 | 域名变更、结构调整 |
三、rewrite 执行顺序
1. 在 server 块中的执行顺序
server {
listen 80;
server_name example.com;
# 第一阶段:server 块中的 rewrite
rewrite ^/old/(.*)$ /new/$1;
rewrite ^/new/(.*)$ /final/$1;
location / {
# 第二阶段:location 块中的 rewrite
rewrite ^/final/(.*)$ /done/$1 last;
}
}
执行顺序:
- 先执行 server 块中的 rewrite(按配置顺序)
- 然后匹配 location
- 最后执行 location 中的 rewrite
2. rewrite 的循环处理
Nginx 对 rewrite 有循环限制(默认 10 次),防止死循环:
# ❌ 错误示例:会导致循环
server {
rewrite ^/(.*)$ /$1; # 自己重写自己
# 错误日志会显示:
# "rewrite or internal redirection cycle"
}
# ✅ 正确示例:加条件避免循环
server {
if ($uri !~ ^/static/) {
rewrite ^/(.*)$ /static/$1 last;
}
}
四、高级应用场景
场景1:基于条件的智能重写
server {
listen 80;
server_name www.example.com;
set $do_rewrite 0;
# 条件1:旧版本文档
if ($request_uri ~ "^/docs/v1/(.*)") {
set $do_rewrite 1;
set $new_path "/docs/current/$1";
}
# 条件2:移动端访问
if ($http_user_agent ~* "(mobile|android|iphone)") {
set $do_rewrite 2;
set $mobile_version "1";
}
# 条件3:特定时间段(配合外部脚本)
if ($time_iso8601 ~ "^(.*)T(09|10|11):") {
set $do_rewrite 3;
set $traffic_type "peak";
}
# 根据条件执行重写
if ($do_rewrite = 1) {
rewrite ^(.*)$ $new_path last;
}
if ($do_rewrite = 2) {
rewrite ^(.*)$ /mobile/$1 last;
}
if ($do_rewrite = 3) {
rewrite ^(.*)$ /peak/$1 last;
}
}
场景2:多语言站点重写
server {
listen 80;
server_name example.com;
# 根据浏览器语言重写
set $lang "en"; # 默认英文
if ($http_accept_language ~* "^zh") {
set $lang "zh";
}
if ($http_accept_language ~* "^ja") {
set $lang "ja";
}
# 如果 URL 没有语言前缀,自动添加
if ($request_uri !~ "^/(zh|en|ja)/") {
rewrite ^/(.*)$ /$lang/$1 last;
}
location / {
root /var/www/$lang;
index index.html;
}
}
场景3:SEO 友好 URL 重写
server {
listen 80;
server_name blog.example.com;
# 文章URL:/post/123/hello-world
# 实际处理:/article.php?id=123&title=hello-world
location /post/ {
rewrite ^/post/(\d+)/([^/]+)/?$ /article.php?id=$1&title=$2 last;
}
# 分类分页:/category/tech/page/2
# 实际处理:/list.php?cat=tech&page=2
location /category/ {
rewrite ^/category/([^/]+)/page/(\d+)/?$ /list.php?cat=$1&page=$2 last;
}
# 标签:/tag/nginx
# 实际处理:/tag.php?tag=nginx
location /tag/ {
rewrite ^/tag/([^/]+)/?$ /tag.php?tag=$1 last;
}
# 日期归档:/2025/01
# 实际处理:/archive.php?year=2025&month=01
location ~ ^/(\d{4})/(\d{2})/?$ {
rewrite ^ /archive.php?year=$1&month=$2 last;
}
# 分页:/page/2
# 实际处理:/index.php?page=2
location /page/ {
rewrite ^/page/(\d+)/?$ /index.php?page=$1 last;
}
}
场景4:防盗链图片重写
server {
listen 80;
server_name images.example.com;
location ~* \.(jpg|png|gif)$ {
# 允许的域名
valid_referers none blocked example.com *.example.com;
if ($invalid_referer) {
# 盗链图片重写为警告图片
rewrite ^(.*)$ /images/anti-theft.jpg last;
}
root /data/images;
expires 30d;
}
# 防止警告图片被重写
location = /images/anti-theft.jpg {
root /data/images;
expires -1;
}
}
场景5:临时维护与流量切换
server {
listen 80;
server_name shop.example.com;
# 维护开关(通过文件存在控制)
set $maintenance_mode 0;
if (-f /var/www/maintenance.on) {
set $maintenance_mode 1;
}
# 排除特定 IP(管理员)
if ($remote_addr ~ "192\.168\.1\.(100|101|102)") {
set $maintenance_mode 0;
}
# 排除健康检查
if ($http_user_agent ~* "health-check") {
set $maintenance_mode 0;
}
# 维护模式开启
if ($maintenance_mode = 1) {
rewrite ^(.*)$ /maintenance.html redirect;
}
# A/B 测试流量切分
split_clients "${remote_addr}${http_user_agent}" $backend_group {
10% "new_version"; # 10% 用户到新版
* "old_version"; # 90% 用户到老版
}
location / {
if ($backend_group = "new_version") {
rewrite ^(.*)$ /new$1 break;
}
proxy_pass http://backend_$backend_group;
}
}
五、rewrite 与 return 的选择
| 场景 | 推荐用 rewrite | 推荐用 return |
|---|---|---|
| 简单域名跳转 | ❌ | ✅ return 301 http://new.com$request_uri; |
| 路径结构重组 | ✅ rewrite ^/old/(.*)$ /new/$1 permanent; | ❌ |
| HTTPS 跳转 | ❌ | ✅ return 301 https://$host$request_uri; |
| 伪静态 | ✅ rewrite ^/article-(\d+).html$ /show.php?id=$1 last; | ❌ |
| 临时维护 | ✅ rewrite ^ /maintain.html redirect; | ✅ return 302 /maintain.html; |
| 复杂条件 | ✅ 可结合 if 使用 | ❌ 无法实现 |
六、性能优化建议
1. 减少 rewrite 数量
# ❌ 多个 rewrite
rewrite ^/a.html /b.html;
rewrite ^/b.html /c.html;
rewrite ^/c.html /d.html;
# ✅ 一次完成
rewrite ^/a.html /d.html;
2. 优先使用 return
# ❌ rewrite 重定向
rewrite ^/(.*)$ http://new.com/$1 permanent;
# ✅ return 更高效
return 301 http://new.com$request_uri;
3. 合理使用 last 和 break
# 不需要重新匹配时用 break
location /download/ {
rewrite ^/download/(.*)\.(exe|msi)$ /dl.php?file=$1.$2 break;
# 后续指令仍执行
proxy_pass http://download_backend;
}
# 需要进入新 location 时用 last
location / {
rewrite ^/old/(.*)$ /new/$1 last;
# 这里的 proxy_pass 不会执行
proxy_pass http://old_backend;
}
七、调试技巧
server {
listen 80;
server_name test.com;
location /debug {
# 添加调试头
add_header X-Original-URI $request_uri;
add_header X-Rewritten-URI $uri;
add_header X-Rewrite-Status "executed";
# 返回变量信息
return 200 "
Original URI: $request_uri
Current URI: $uri
Args: $args
";
}
# 测试各种 rewrite
location /test/ {
rewrite ^/test/(\d+).html$ /debug?rewrite=yes&id=$1 last;
}
}
访问 /test/123.html 就能看到完整的 rewrite 效果
发表回复