Nginx Rewrite 基础教程:URL 重写的艺术与实践
引言
在现代 Web 开发和服务器管理中,对访问的 URL 进行处理、转换或重定向是司空见惯的需求。无论是为了 SEO 优化、用户体验提升、系统架构调整,还是简单的路径美化,URL 重写都扮演着核心角色。Nginx 作为一款高性能的 Web 服务器和反向代理,其强大的 URL 重写功能是其核心竞争力之一。
Nginx 的 Rewrite 模块允许管理员通过配置规则,根据请求的 URI、Host、User-Agent 等信息,动态地修改请求的 URL,然后 Nginx 会根据新的 URL 继续处理请求(内部重写)或通知客户端进行跳转(外部重定向)。掌握 Nginx Rewrite 是每一个 Nginx 使用者必备的技能。
然而,由于其强大的功能和基于正则表达式的匹配方式,Rewrite 配置有时也显得复杂且容易出错,甚至可能导致意外的循环重定向。因此,深入理解其工作原理和语法规则至关重要。
本文将详细介绍 Nginx Rewrite 的基础知识,包括正则表达式、rewrite
指令的语法、各种标志(flags)的作用、Rewrite 的处理流程以及如何在不同的上下文中应用和调试 Rewrite 规则,并通过丰富的实例帮助读者掌握这一强大的工具。
一、理解 URL 重写的概念
简单来说,URL 重写就是服务器接收到一个 URL 请求后,在内部或外部将其转换为另一个 URL 的过程。
- 内部重写 (Internal Rewrite): 服务器接收到请求
/old/path
,通过配置规则将其转换为/new/path
,然后服务器内部直接去查找和处理/new/path
对应的资源。客户端对此过程是无感的,浏览器地址栏显示的仍是/old/path
。 - 外部重定向 (External Redirect): 服务器接收到请求
/old/page.html
,通过配置规则判断需要重定向到/new/page.html
。服务器会向客户端发送一个特殊的 HTTP 响应(通常是 301 或 302 状态码),告诉浏览器新的地址是/new/page.html
。浏览器收到响应后,会向新地址发起新的请求。此时,浏览器地址栏会显示/new/page.html
。
Nginx 的 rewrite
指令可以实现这两种类型的重写。
二、Nginx Rewrite 的基石:正则表达式
Nginx Rewrite 规则的核心在于使用正则表达式(Regular Expressions)来匹配请求的 URI。掌握常用的正则表达式语法是编写有效 Rewrite 规则的前提。
以下是一些 Nginx Rewrite 中常用的正则表达式元字符和语法:
.
: 匹配除换行符以外的任意单个字符。*
: 匹配前一个字符零次或多次。+
: 匹配前一个字符一次或多次。?
: 匹配前一个字符零次或一次(使其变为可选)。也可以用于使*
或+
的匹配变为“非贪婪”模式,尽可能少地匹配。^
: 匹配行的开头。在 Nginx 中通常匹配 URI 的开头。$
: 匹配行的结尾。在 Nginx 中通常匹配 URI 的结尾。|
: 或操作符,匹配|
前或后的表达式。例如:GET|POST
匹配 “GET” 或 “POST”。()
: 捕获组。将括号内的匹配内容捕获到一个变量中,可以在替换字符串中使用$1
,$2
, …,$9
来引用这些捕获的内容。$1
引用第一个捕获组的内容,$2
引用第二个,以此类推。[]
: 字符集。匹配方括号内的任意一个字符。例如:[abc]
匹配 ‘a’, ‘b’, 或 ‘c’。[0-9]
匹配任意数字,[a-zA-Z]
匹配任意字母。[^...]
: 否定字符集。匹配不在方括号内的任意一个字符。例如:[^/]
匹配除斜杠以外的任意字符。\d
: 匹配任意数字,等同于[0-9]
。\w
: 匹配任意字母、数字或下划线,等同于[a-zA-Z0-9_]
。\s
: 匹配任意空白字符。\
: 转义字符。用于匹配具有特殊含义的元字符本身,例如\.
匹配点号,\/
匹配斜杠。
在 Nginx Rewrite 中,正则表达式通常用于匹配请求的 URI(不包含域名和查询字符串)。 例如,对于请求 http://example.com/article/123?foo=bar
,Rewrite 规则通常匹配的部分是 /article/123
。
示例:
- 匹配以
/old-path
开头的 URI:^/old-path
- 匹配精确的
/about.html
:^/about\.html$
(注意点号需要转义) - 匹配
/article/
后跟一个或多个数字的 URI,并捕获这些数字:^/article/(\d+)$
- 匹配
/download/
后跟任意文件名和扩展名,并捕获文件名(不含扩展名):^/download/(.*)\.zip$
三、rewrite
指令的语法
rewrite
指令是 Nginx Rewrite 模块的核心。其基本语法如下:
nginx
rewrite regex replacement [flag];
让我们详细解析这三个部分:
regex
(正则表达式): 用于匹配请求 URI 的正则表达式。如果 URI 匹配了这个正则表达式,Nginx 就会执行相应的重写规则。-
replacement
(替换字符串): 当regex
匹配成功时,用于替换原始 URI 的新字符串。这个字符串可以包含:- 静态文本:例如
/new/path/
。 - 捕获组引用:使用
$1
,$2
, … 来引用regex
中捕获组()
匹配的内容。 - Nginx 内置变量:例如
$uri
,$request_uri
,$host
,$server_port
,$args
等。$uri
: 当前请求的 URI,不包含域名和查询字符串。$request_uri
: 完整的原始请求 URI,包含查询字符串(例如/path?arg=value
)。$host
: 请求头中的 Host 字段,如果不存在则为服务器名。$args
: 请求中的查询字符串。$is_args
: 如果请求中带有参数,值为?
,否则为空。常用于拼接查询字符串。
- 组合:静态文本、捕获组引用和 Nginx 变量可以自由组合。
注意: 如果
replacement
以http://
,https://
, 或$scheme
开头,则表示这是一个外部重定向。否则,这是一个内部重写。 - 静态文本:例如
-
flag
(标志): 可选参数,用于控制 Rewrite 规则的行为。这是理解 Nginx Rewrite 复杂性的关键部分。常用的标志有四个:-
last
:- 行为: 完成当前的 Rewrite 规则后,停止处理当前
location
块中的剩余 Rewrite 规则,然后使用新的 URI 重新查找(internal redirect)匹配的location
块。 - 用途: 常用于在
server
块中或location
块的末尾进行内部重写,将请求导向到一个新的location
块进行处理。例如,将伪静态 URL 重写为内部脚本路径。 - 注意: 如果
last
用在server
块中,它会导致 Nginx 使用新的 URI 再次尝试匹配server
块中的location
块。如果用在location
块中,它也会导致 Nginx 使用新的 URI 再次尝试匹配server
块中的location
块(从头开始)。这正是它叫做last
(最后的)的原因——它结束了当前这一轮的 URI 处理和location
匹配,并以新的 URI 启动新的一轮。
- 行为: 完成当前的 Rewrite 规则后,停止处理当前
-
break
:- 行为: 完成当前的 Rewrite 规则后,停止处理当前
location
块中的剩余 Rewrite 规则。不会使用新的 URI 重新查找location
块。Nginx 将根据新的 URI 在当前location
块内继续进行其他指令的处理(例如root
,index
,try_files
)。 - 用途: 常用于在特定的
location
块中停止后续 Rewrite 处理,直接处理当前请求。例如,在/static/
这样的location
中,你可能想做一些简单的重写(如去掉版本号),然后直接提供文件,而不希望它跳到其他location
。 - 注意:
break
只停止当前location
块内的 Rewrite 处理,不会影响server
块中的 Rewrite 或其他location
块。它也不会导致 Nginx 重新匹配location
。
- 行为: 完成当前的 Rewrite 规则后,停止处理当前
-
redirect
:- 行为: 执行一个外部重定向,使用 302 Found 状态码。新的 URL 会在响应头的
Location
字段中返回给客户端。 - 用途: 用于临时性的 URL 重定向。客户端浏览器地址栏会显示新的 URL。
- 注意:
replacement
必须以http://
,https://
, 或$scheme
开头,或者是一个完整的路径(不建议只写路径,可能导致不完整 URL 的重定向)。通常与完整的 URL 一起使用。
- 行为: 执行一个外部重定向,使用 302 Found 状态码。新的 URL 会在响应头的
-
permanent
:- 行为: 执行一个外部重定向,使用 301 Moved Permanently 状态码。新的 URL 会在响应头的
Location
字段中返回给客户端。 - 用途: 用于永久性的 URL 重定向。搜索引擎会根据 301 状态码更新其索引,并将旧 URL 的权重转移到新 URL。客户端浏览器地址栏会显示新的 URL。
- 注意: 与
redirect
类似,replacement
通常需要是一个完整的 URL 或以http://
,https://
,$scheme
开头。
- 行为: 执行一个外部重定向,使用 301 Moved Permanently 状态码。新的 URL 会在响应头的
-
last
vs break
总结:
标志 | 是否停止当前块的 Rewrite | 是否重新查找 location |
在当前块内继续处理 | 主要用途 |
---|---|---|---|---|
last |
是 | 是 | 否 | 将请求转发到另一个 location |
break |
是 | 否 | 是 | 在当前 location 内完成处理 |
理解这两个标志的区别是避免 Rewrite 配置错误的关键。last
是“跳出去,再匹配一次”,而 break
是“停下来,就在这处理”。
redirect
vs permanent
总结:
标志 | 状态码 | 搜索引擎处理方式 | 浏览器缓存 | 用途 |
---|---|---|---|---|
redirect |
302 | 不会更新索引 | 不缓存 | 临时重定向 |
permanent |
301 | 更新索引,传递权重 | 可能永久缓存 | 永久重定向 |
四、rewrite
指令的上下文(Contexts)
rewrite
指令可以在 server
, location
, 和 if
块中使用。它们在不同上下文中的行为略有差异:
-
server
块:- 在
server
块中的rewrite
指令会在 Nginx 找到匹配的server
后、查找location
块之前执行。 server
块中的 Rewrite 会按顺序逐条执行。- 如果某条
rewrite
规则使用了last
标志,Nginx 会根据新的 URI 重新查找location
块(从server
块的开头重新匹配location
)。 - 如果使用了
break
,redirect
, 或permanent
标志,或者规则没有匹配,Nginx 会继续处理后续指令(包括查找location
)。
- 在
-
location
块:- 在
location
块中的rewrite
指令会在 Nginx 找到匹配的location
后执行。 location
块中的 Rewrite 也会按顺序逐条执行。- 如果某条
rewrite
规则使用了last
标志,Nginx 会根据新的 URI 重新查找location
块(从server
块的开头重新匹配location
)。这与在server
块中使用last
的行为是一致的。 - 如果某条
rewrite
规则使用了break
标志,Nginx 会停止处理当前location
块中剩余的 Rewrite 规则,然后继续处理当前location
块中的其他指令 (root
,index
,try_files
等)。不会重新查找location
。 - 如果使用了
redirect
或permanent
标志,Nginx 会执行外部重定向并停止处理。 - 如果规则没有匹配,Nginx 会继续处理当前
location
块中的下一条 Rewrite 规则,或者处理当前location
块中的其他指令。
- 在
-
if
块:rewrite
指令也可以放在if
块内部。if
指令允许基于更复杂的条件(如变量值)来执行 Rewrite 或其他指令。if
块中的rewrite
指令的行为与在location
块中类似,但只在if
条件为真时执行。
Rewrite 处理顺序总结:
Nginx 处理一个请求时,Rewrite 规则的执行顺序大致如下:
- 处理
server
块中的rewrite
指令(按顺序)。 - 如果
server
块中的 Rewrite 规则使用了last
标志,则使用新的 URI 再次从第 1 步开始处理(但通常只会在查找location
前发生)。 - 查找匹配的
location
块。 - 处理匹配到的
location
块中的rewrite
指令(按顺序)。这包括location
内的if
块中的 Rewrite。 - 如果
location
块中的 Rewrite 规则使用了last
标志,则使用新的 URI 再次从第 3 步开始处理(重新查找location
)。 - 如果
location
块中的 Rewrite 规则使用了break
标志,则停止当前块的 Rewrite 处理,在当前location
内继续执行其他指令。 - 如果使用了
redirect
或permanent
,则执行外部重定向并终止。 - 如果没有 Rewrite 发生或 Rewrite 处理完毕(没有
last
或break
),Nginx 继续处理当前location
块内的其他指令(如root
,index
,try_files
,proxy_pass
等)。
重要提示: 避免在一个 location
块中混合使用 rewrite
和 proxy_pass
指令。虽然在某些情况下可以工作,但这通常不是最佳实践,且可能导致难以预料的行为。通常的最佳做法是使用 rewrite ... last;
将请求重写并转发到另一个 location
块,然后在那个块中使用 proxy_pass
。或者,对于简单的转发,直接在 location
块中使用 proxy_pass
配合匹配规则。
五、实践示例
以下是一些常见的 Nginx Rewrite 使用场景及其配置示例:
示例 1:简单的路径重写 (内部重写)
将 /old-path/
重写为 /new-path/
。
“`nginx
location /old-path/ {
rewrite ^/old-path/(.)$ /new-path/$1 last;
# 或者
# rewrite ^/old-path/(.)$ /new-path/$1 break; # 如果 /new-path/ 也是当前 location 处理
}
location /new-path/ {
# 处理 /new-path/ 的配置,例如
# root /var/www/html/new;
# index index.html;
}
``
^/old-path/(.*)$
**解释:**
*匹配以
/old-path/开头,后面跟任意内容,并将后面内容捕获到
$1。
/new-path/$1
*是替换字符串,将
/old-path/替换为
/new-path/,并带上捕获的子路径。
last
*标志使得 Nginx 使用
/new-path/…重新查找
location,最终匹配到
location /new-path/` 进行处理。
示例 2:永久重定向旧页面到新页面 (外部重定向)
将 /old-page.html
永久重定向到 /new-page.html
。
“`nginx
server {
listen 80;
server_name example.com;
rewrite ^/old-page\.html$ /new-page.html permanent; # 在 server 块或 location 块都可以
location / {
# 其他配置
}
location /new-page.html {
# 处理新页面的配置
}
}
``
^/old-page.html$
**解释:**
*精确匹配
/old-page.html。
/new-page.html
*是替换字符串。由于它不是以
http://等开头,Nginx 默认会构建一个完整的 URL 进行重定向(例如
http://example.com/new-page.html)。
permanent` 标志发送 301 状态码进行永久重定向。
*
更严谨的永久重定向写法(推荐):
nginx
rewrite ^/old-page\.html$ $scheme://$host/new-page.html permanent;
解释: 使用 $scheme
和 $host
确保重定向后的 URL 包含正确的协议和域名,避免潜在问题。
示例 3:规范化 URL – 添加或移除 www
将 example.com
的访问重定向到 www.example.com
。
“`nginx
server {
listen 80;
listen 443 ssl;
server_name example.com; # 匹配 non-www
# 永久重定向到 www
rewrite ^(.*)$ $scheme://www.example.com$1 permanent;
# 或者
# return 301 $scheme://www.example.com$request_uri; # 更简单的方式,无需 regex
# 其他配置...
}
server {
listen 80;
listen 443 ssl;
server_name www.example.com; # 处理 www 的请求
# 其他配置...
}
``
server
**解释:**
* 第一个块监听
example.com。
rewrite ^(.*)$
*匹配所有请求 URI,并将其捕获到
$1。
$scheme://www.example.com$1
*构建新的 URL,将域名改为
www.example.com。
permanent` 标志发送 301 状态码。
*
示例 4:强制 HTTPS
将所有 HTTP 请求重定向到 HTTPS。
“`nginx
server {
listen 80;
server_name example.com www.example.com;
# 将所有 HTTP 请求重定向到 HTTPS
rewrite ^(.*)$ https://$host$1 permanent;
# 或者
# return 301 https://$host$request_uri; # 更简单的方式,推荐用于简单重定向
# 其他配置...
}
server {
listen 443 ssl;
server_name example.com www.example.com;
# HTTPS 配置...
}
``
server
**解释:**
* 第一个块监听 80 端口(HTTP)。
rewrite ^(.*)$
*匹配所有请求 URI。
https://$host$1
*构建新的 URL,将协议改为
https,域名保持不变,带上原始 URI。
permanent` 标志发送 301 状态码。
*
示例 5:美化动态 URL (伪静态)
将 /article/123
这样的 URL 内部重写为 /news.php?id=123
。
“`nginx
location /article/ {
rewrite ^/article/(\d+)$ /news.php?id=$1 last;
}
location ~ .php$ {
# PHP 相关的处理配置,例如 fastcgi_pass
# include fastcgi_params;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# fastcgi_pass unix:/var/run/php-fpm.sock; # 或者 fastcgi_pass 127.0.0.1:9000;
}
``
location /article/
**解释:**
*匹配以
/article/开头的请求。
^/article/(\d+)$
*匹配
/article/后跟一个或多个数字,并将数字捕获到
$1。
/news.php?id=$1
*是替换字符串,构建新的内部 URI,将捕获的数字作为
id参数。
last
*标志使得 Nginx 使用
/news.php?id=…重新查找
location,从而匹配到处理
.php文件的
location` 块。
示例 6:移除 URL 中的 /index.html
将 /index.html
重定向到 /
。
“`nginx
server {
listen 80;
server_name example.com;
# 在 server 块或 location / 块都可以
rewrite ^/index\.html$ / permanent;
location / {
# 其他配置,例如
root /var/www/html;
index index.html index.htm; # Nginx 默认也会处理 / 请求并查找 index 文件
}
}
``
^/index.html$
**解释:**
*精确匹配
/index.html。
/
*是替换字符串。
permanent` 标志发送 301 状态码。
*
示例 7:使用 break
停止 Rewrite 处理
假设你有一个 /static/
目录,你只想对其内部进行简单的重写,然后直接提供文件。
“`nginx
location /static/ {
# 假设你想把 /static/v1.0/css/style.css 改写成 /static/css/style.css 提供
rewrite ^/static/v\d+.\d+/(.*)$ /static/$1 break;
root /var/www/static;
# try_files $uri $uri/ =404;
}
``
location /static/
**解释:**
*匹配静态文件请求。
rewrite ^/static/v\d+.\d+/(.*)$ /static/$1 break;
*匹配带有版本号路径的请求,并移除版本号,捕获子路径到
$1。
/static/$1
*是替换字符串。
break
*标志停止了当前
location块中的 Rewrite 处理。Nginx 将使用新的 URI(例如
/static/css/style.css)在当前的
location /static/块中继续处理,结合
root /var/www/static;指令,最终尝试提供
/var/www/static/static/css/style.css` 文件。
六、Rewrite 的替代方案与注意事项
虽然 rewrite
指令功能强大,但有时它并不是唯一的选择,甚至不是最佳选择。
-
return
指令: 对于简单的外部重定向,return
指令通常更简洁高效。它可以直接返回指定的 HTTP 状态码和 URL,无需使用正则表达式。
“`nginx
# 重定向 /old 到 /new (301)
location = /old {
return 301 /new;
}重定向 /old-path/ 到 /new-path/ (302)
location /old-path/ {
return 302 /new-path/$request_uri; # $request_uri 包含完整路径和参数
}
``
return指令的执行效率通常比
rewrite` 更高,且更易读,对于纯粹的重定向场景优先考虑使用它。 -
try_files
指令:try_files
指令主要用于检查文件或目录是否存在,并根据结果执行内部重写或返回错误。它在处理静态文件和伪静态场景中非常有用,可以部分替代rewrite ... last;
的功能。
“`nginx
location / {
# 尝试查找 $uri 对应的文件,如果不存在,尝试查找 $uri/ 对应的目录下的 index 文件
# 如果都不存在,内部重写到 /index.php 并带上查询参数
try_files $uri $uri/ /index.php?$args;
}location ~ .php$ {
# 处理 PHP 请求
# …
}
``
/some/path
这个例子中,会尝试查找文件
/some/path,然后尝试查找目录
/some/path/下的索引文件。如果都找不到,则内部重写到
/index.php?args。这种方式比使用
rewrite` 规则来判断文件是否存在更直观。 -
if
指令: 虽然rewrite
可以在if
块中使用,但 Nginx 的官方文档建议谨慎使用if
指令,尤其是在location
块中,因为它的行为有时可能出乎意料,尤其与其他指令(如rewrite
,try_files
,proxy_pass
)结合使用时。对于复杂的条件逻辑,考虑将配置拆分到不同的server
块或location
块,或者使用map
指令来设置变量。
注意事项:
- 正则表达式的准确性: 错误的正则表达式是导致 Rewrite 失败或意外匹配的最常见原因。仔细测试你的正则表达式。
- 标志的选择: 正确选择
last
,break
,redirect
,permanent
标志至关重要,它们直接影响 Rewrite 的行为和后续请求处理流程。 - Rewrite 循环: 不正确的 Rewrite 规则可能导致无限循环重定向,耗尽服务器资源并导致客户端浏览器报错。仔细检查规则,确保不会将 URI 重写回一个会再次被同一或另一条规则匹配的 URI。使用
permanent
时尤其小心,因为它会被浏览器缓存。 - 处理顺序: 理解 Nginx 处理 Rewrite 规则以及与其他指令(如
try_files
,location
匹配)的交互顺序非常重要。 - 调试: 当 Rewrite 不工作时,可以增加 Nginx 的错误日志级别(例如到
info
或debug
,但debug
级别日志量巨大,通常只在测试环境使用),并在error_log
文件中查找与rewrite
相关的输出,这通常能提供关于规则匹配和处理过程的有价值信息。
七、总结
Nginx Rewrite 是一个功能强大的 URL 处理工具,它基于正则表达式,通过 rewrite
指令及其标志,可以实现灵活的内部重写和外部重定向。掌握 regex
, replacement
, 和 flag
的语法及其在不同上下文(server
, location
, if
)中的行为,是高效配置 Nginx 的关键。
尽管 Rewrite 功能强大,但在简单的重定向场景中,优先考虑使用更直接的 return
指令。对于文件查找和伪静态处理,try_files
往往是更清晰的选择。合理结合使用这些指令,并理解它们之间的处理顺序,可以帮助你构建健壮、高效且易于维护的 Nginx 配置。
通过本文的介绍和示例,希望你能对 Nginx Rewrite 有一个全面而深入的理解。实践是最好的老师,尝试在你的 Nginx 环境中配置和测试不同的 Rewrite 规则,逐步掌握这项重要的技能。记住,小心、耐心、反复测试是避免 Rewrite 陷阱的最佳途径。