
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Nginx这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Nginx – ngx_http_rewrite_module 模块:URL 重写高级用法 🚀
-
- 一、基础回顾:rewrite 指令的本质 🧩
-
- ✅ 常见 flag 含义
- 🌰 基础示例
- 二、高级用法一:条件判断与嵌套重写 🧠
-
- 📌 场景:移动端与 PC 端自动跳转
- 📌 场景:基于 Referer 的防盗链 + 重定向
- 📌 场景:语言/地区自动路由(多语言站点)
- 三、高级用法二:变量与正则捕获的深度联动 🔗
-
- 📌 场景:动态路径参数重写(API 版本控制)
- 📌 场景:参数重组与过滤(SEO 优化)
- 四、高级用法三:map 指令 —— 高性能的键值映射引擎 🚀
-
- 📌 场景:灰度发布 —— 按用户 ID 分流
- 📌 场景:CDN 缓存策略动态控制
- 五、高级用法四:正则表达式陷阱与优化技巧 🛠️
-
- ❌ 错误写法:过度贪婪匹配
- ✅ 优化写法:使用非贪婪 + 精确字符集
- ✅ 推荐:使用锚点 `^` 和 `$` 精确匹配
- ✅ 推荐:使用 `~` 而非 `~*` 当大小写敏感时
- ✅ 推荐:使用 `try_files` 替代冗余 rewrite
- 六、高级用法五:与 Java 后端联动的完整案例 🤝
-
- 📁 Java 后端服务(Spring Boot 示例)
- 📄 Nginx 配置(完整联动)
- 🔍 实际请求示例
- 📊 日志示例(`/var/log/nginx/api-access.log`)
- 七、高级用法六:动态重写 + Lua 扩展(OpenResty)🚀
-
- 📌 场景:根据用户 Session ID 从 Redis 获取权限,动态重写路径
- 🔗 与 Java 的联动场景
- 八、高级用法七:URL 重写与缓存失效策略 🔄
-
- 📌 场景:版本化静态资源 + 自动缓存刷新
- 📌 场景:配合 CI/CD 实现自动缓存刷新
- 九、高级用法八:多层重写与嵌套 location 的控制流 🧩
-
- 📌 场景:混合重写 + 嵌套 location 实现复杂路由
- 📊 Mermaid 图表:Nginx 请求处理流程
- 十、实战演练:构建一个完整的微服务网关 🏗️
-
- 📁 配置文件:`/etc/nginx/conf.d/gateway.conf`
- 📊 Mermaid 图表:网关请求处理流程
- 十一、性能优化与调试技巧 🔍
-
- ✅ 1. 测试重写规则
- ✅ 2. 开启调试日志(谨慎使用)
- ✅ 3. 使用 `echo` 模块调试(需安装 ngx_http_echo_module)
- ✅ 4. 监控重写次数
- 十二、常见错误与避坑指南 ⚠️
-
- 📌 经典陷阱:`rewrite ^(.*)$ /index.html last;` 导致死循环
- 十三、总结:何时使用 rewrite?何时不用?✅
- 十四、延伸阅读与推荐资源 📚
- 十五、结语:重写不是魔法,是艺术 🎨
- 附录:Java 服务完整示例(可直接运行) 🖥️
- 🌟 最后:你,就是架构师
Nginx – ngx_http_rewrite_module 模块:URL 重写高级用法 🚀
在现代 Web 架构中,URL 重写早已不再是简单的“跳转”或“美化链接”的小技巧,它已成为高可用、高安全、高可维护性系统架构中的核心组件之一。Nginx 作为全球最流行的反向代理与 Web 服务器之一,其内置的 ngx_http_rewrite_module 模块,提供了强大、灵活、高性能的 URL 重写能力。本文将深入剖析该模块的高级用法,结合真实业务场景,带你从基础语法一路进阶到复杂逻辑控制、正则表达式嵌套、条件判断组合、与后端 Java 服务联动的完整解决方案。
我们将不只停留在“怎么写”,更要理解“为什么这么写”、“在什么场景下最优”、“如何避免踩坑”。文末还将提供 Java 代码示例,展示如何与 Nginx 的重写规则协同工作,构建前后端分离、动态路由、灰度发布、A/B 测试等现代应用架构。
一、基础回顾:rewrite 指令的本质 🧩
在深入高级用法前,我们先快速回顾 rewrite 指令的基本结构:
rewrite regex replacement [flag];
- regex:正则表达式,用于匹配请求的 URI
- replacement:替换后的目标 URI
- flag:控制重写行为的标志位,如 last、break、redirect、permanent
✅ 常见 flag 含义
| last | 停止当前 rewrite 处理,重新开始 location 匹配 | 🔁 重新查找 location 块 |
| break | 停止当前 rewrite 处理,继续当前 location 内处理 | 🛑 不再查找其他 location |
| redirect | 返回 302 临时重定向 | 🚨 浏览器地址栏变化 |
| permanent | 返回 301 永久重定向 | 🏁 浏览器缓存,SEO 友好 |
⚠️ 注意:last 和 break 不会触发客户端重定向,仅在服务端内部处理;而 redirect 和 permanent 会返回 HTTP 状态码给客户端,触发浏览器跳转。
🌰 基础示例
server {
listen 80;
server_name example.com;
# 将 /old-page 重写为 /new-page
rewrite ^/old-page$ /new-page last;
# 将所有 .html 请求重定向到无扩展名路径
rewrite ^(.+)\\.html$ $1 permanent;
# 将带参数的旧链接重定向到新结构
rewrite ^/product/(\\d+)$ /item?id=$1 last;
}
这些是入门级用法,但真正的威力,在于组合、嵌套、条件、变量、逻辑判断的高级运用。
二、高级用法一:条件判断与嵌套重写 🧠
Nginx 的 if 指令虽然常被警告“不要滥用”,但在可控场景下,它是实现复杂业务逻辑的利器。结合 rewrite,我们可以实现:
- 基于 User-Agent 的设备重定向
- 基于 Referer 的防爬虫策略
- 基于地域或语言的多语言站点路由
- 基于请求头的灰度发布
📌 场景:移动端与 PC 端自动跳转
假设你有一个网站,移动端使用 m.example.com,PC 端使用 www.example.com。但用户可能直接访问 www.example.com 并从手机打开,此时你希望自动跳转到移动端。
server {
listen 80;
server_name www.example.com;
# 如果是移动设备,重写到 m.example.com
if ($http_user_agent ~* "(android|iphone|ipod|blackberry|windows phone)") {
rewrite ^(.*)$ http://m.example.com$1 redirect;
}
# 其他情况正常处理
location / {
root /var/www/html;
index index.html;
}
}
💡 为什么用 ~*? ~* 表示不区分大小写的正则匹配,避免因 UA 中大小写不同导致识别失败。
📌 场景:基于 Referer 的防盗链 + 重定向
你希望防止外部网站直接链接你的图片资源(如 https://example.com/images/logo.png),但允许百度、Google 等搜索引擎访问。
location ~* \\.(jpg|jpeg|png|gif|webp)$ {
valid_referers none blocked server_names
*.google.com
*.baidu.com
*.bing.com;
if ($invalid_referer) {
rewrite ^(.*)$ https://example.com/static/steal-protection.jpg redirect;
}
root /var/www/images;
expires 30d;
}
🔗 你可以在 https://www.google.com 搜索 “Nginx防盗链” 查看实际效果,许多大型网站都采用类似策略。
📌 场景:语言/地区自动路由(多语言站点)
你的网站支持中文、英文、日文,用户访问 example.com 时,根据 Accept-Language 自动跳转:
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";
}
rewrite ^/$ /$lang/ redirect;
}
此时:
- Accept-Language: zh-CN,zh;q=0.9 → 重定向到 /zh/
- Accept-Language: ja-JP → 重定向到 /ja/
- 其他 → /en/
✅ 注意:set 指令是 Nginx 变量赋值的关键,它允许你在 if 中动态设置变量,供后续 rewrite 使用。
三、高级用法二:变量与正则捕获的深度联动 🔗
Nginx 支持大量内置变量,如 $request_uri、$args、$scheme、$host、$server_port 等。更重要的是,正则表达式中的捕获组(括号 ())可以被 $1、$2…$9 引用。
📌 场景:动态路径参数重写(API 版本控制)
你有多个 API 版本:
- /api/v1/user/123 → 转发到 /api/v1/user?id=123
- /api/v2/user/456 → 转发到 /api/v2/user?id=456
但你不想为每个版本写一条规则,而是希望动态提取版本号和 ID。
location /api/ {
# 匹配 /api/v{数字}/user/{数字}
if ($request_uri ~ ^/api/v(\\d+)/user/(\\d+)$) {
set $version $1;
set $userid $2;
rewrite ^ /api/$version/user?id=$userid last;
}
# 默认处理
proxy_pass http://backend-api;
}
✅ 这里我们使用了两个捕获组:(\\d+) 匹配版本号,(\\d+) 匹配用户 ID,分别赋值给 $version 和 $userid,再用于重写。
📌 场景:参数重组与过滤(SEO 优化)
原始 URL:/search?q=java&sort=price&category=tech&page=1
你希望:
- 移除无用参数(如 utm_source)
- 重新排序参数(按字母顺序)
- 重写为 /search/java/tech?sort=price&page=1
这需要多个重写步骤 + 变量拼接:
location /search {
# 1. 提取 q 参数
if ($args ~ "(^|&)q=([^&]*)&") {
set $search_term $2;
}
# 2. 提取 category 参数
if ($args ~ "(^|&)category=([^&]*)&") {
set $category $2;
}
# 3. 提取 sort 参数
if ($args ~ "(^|&)sort=([^&]*)&") {
set $sort_param $2;
}
# 4. 提取 page 参数
if ($args ~ "(^|&)page=([^&]*)&") {
set $page_param $2;
}
# 5. 重构路径:/search/{q}/{category}?sort=…&page=…
rewrite ^ /search/$search_term/$category?sort=$sort_param&page=$page_param last;
}
⚠️ 警告:以上写法虽然可行,但性能较差,因为每个 if 都会执行一次正则匹配。在高并发场景下,建议改用 map 指令(见下文)。
四、高级用法三:map 指令 —— 高性能的键值映射引擎 🚀
map 指令是 Nginx 中最被低估的性能利器。它在配置加载时预编译,运行时无正则匹配开销,适合大量静态映射、动态路由、灰度策略。
📌 场景:灰度发布 —— 按用户 ID 分流
你有 10% 的用户参与新功能灰度测试,希望他们访问 /v2/ 接口,其余访问 /v1/。
map $http_cookie $api_version {
default "v1";
"~*userid=12345" "v2";
"~*userid=67890" "v2";
"~*userid=11223" "v2";
"~*userid=(\\d+)" "v2"; # 匹配任意用户ID,用于测试
}
server {
listen 80;
server_name api.example.com;
# 根据 $api_version 重写路径
rewrite ^/api/(.*)$ /api/$api_version/$1 last;
location ~ ^/api/(v1|v2)/ {
proxy_pass http://backend_$1;
}
}
✅ 优势:
- 无需在每个请求中执行正则
- 支持正则匹配(~*)
- 可以引用 Cookie、Header、IP、变量等
- 配置加载时编译,性能极高
📌 场景:CDN 缓存策略动态控制
你希望对某些路径不缓存(如 /cart、/checkout),其余缓存 1 小时。
map $request_uri $cache_control {
default "public, max-age=3600";
"~^/cart" "no-cache";
"~^/checkout" "no-cache";
"~^/api/auth" "private, max-age=60";
}
server {
listen 80;
server_name cdn.example.com;
location / {
add_header Cache-Control $cache_control;
proxy_pass http://origin;
proxy_cache my_cache;
proxy_cache_valid 200 1h;
}
}
🌐 你可以参考 https://www.nginx.com/blog/nginx-caching-guide/ 了解 Nginx 缓存最佳实践。
五、高级用法四:正则表达式陷阱与优化技巧 🛠️
正则表达式是强大的,但也是性能杀手。以下是一些必须避免的错误写法和优化建议。
❌ 错误写法:过度贪婪匹配
# 危险!
rewrite ^(.*)/product/(\\d+)/(.*)$ /item?id=$2 last;
如果请求是 /a/b/c/product/123/d/e/f,(.*) 会匹配到 /a/b/c/,然后 (\\d+) 匹配 123,(.*) 匹配 /d/e/f —— 看似正确,但性能极差。
✅ 优化写法:使用非贪婪 + 精确字符集
# 推荐!
rewrite ^[^/]*+/product/(\\d+)/.*$ /item?id=$1 last;
[^/]*+ 表示“匹配任意非 / 字符,且为占有量词(避免回溯)”,性能提升 3~5 倍。
✅ 推荐:使用锚点 ^ 和 $ 精确匹配
# 好
rewrite ^/login$ /auth/login.html last;
# 差(会匹配 /login?abc 和 /login/user)
rewrite /login /auth/login.html last;
✅ 推荐:使用 ~ 而非 ~* 当大小写敏感时
# 如果你确定 UA 是小写,用 ~ 更快
if ($http_user_agent ~ "android") { … }
# 仅当不确定时才用 ~*
if ($http_user_agent ~* "android") { … }
✅ 推荐:使用 try_files 替代冗余 rewrite
# 不推荐
rewrite ^/images/(.*)$ /static/images/$1 last;
# 推荐
location /images/ {
try_files $uri /static/images/$uri =404;
}
try_files 是为文件系统查找设计的,比 rewrite 更轻量、更快。
六、高级用法五:与 Java 后端联动的完整案例 🤝
Nginx 不是孤岛。它常作为 Java 应用(Spring Boot、Tomcat、Jetty)的前置网关。我们来构建一个真实业务场景:
需求: 用户访问 /api/v1/user/123,Nginx 需要:
📁 Java 后端服务(Spring Boot 示例)
@RestController
@RequestMapping("/internal/v1/user")
public class UserController {
@GetMapping
public ResponseEntity<User> getUser(@RequestParam("id") Long userId,
@RequestHeader("X-API-Key") String apiKey) {
// 模拟白名单校验(实际应从 Redis 或 DB 加载)
Set<String> validKeys = Set.of("key123", "key456", "key789");
if (!validKeys.contains(apiKey)) {
return ResponseEntity.status(403).body(null);
}
User user = new User(userId, "张三", "zhangsan@example.com");
return ResponseEntity.ok(user);
}
// 简化 User 类
static class User {
private Long id;
private String name;
private String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// getter/setter 省略
}
}
📄 Nginx 配置(完整联动)
# 定义 API Key 白名单(使用 map)
map $http_x_api_key $is_valid_api_key {
default 0;
"key123" 1;
"key456" 1;
"key789" 1;
"~^key\\d{3}$" 1; # 匹配 key100~key999
}
server {
listen 80;
server_name api.example.com;
# 访问日志格式:时间、IP、URI、API-Key、状态、重写路径
log_format api_access '$remote_addr – [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_api_key" "$rewritten_uri"';
access_log /var/log/nginx/api-access.log api_access;
# 主要重写逻辑
location /api/v1/user/ {
# 提取用户ID
if ($request_uri ~ ^/api/v1/user/(\\d+)$) {
set $user_id $1;
set $rewritten_uri "/internal/v1/user?id=$user_id";
}
# 如果 API Key 无效,直接拒绝
if ($is_valid_api_key = 0) {
return 403 "Forbidden: Invalid API Key";
}
# 重写并传递参数
rewrite ^ /api/v1/user/ last;
# 转发到后端
proxy_pass http://localhost:8080$rewritten_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-API-Key $http_x_api_key;
}
# 通用错误处理
error_page 403 /403.html;
location = /403.html {
internal;
root /usr/share/nginx/html;
}
}
🔍 实际请求示例
curl -H "X-API-Key: key123" http://api.example.com/api/v1/user/123
# → Nginx 重写为:/internal/v1/user?id=123 → 转发到 Java 服务 → 返回用户信息
curl -H "X-API-Key: invalid_key" http://api.example.com/api/v1/user/123
# → 返回 403 Forbidden
📊 日志示例(/var/log/nginx/api-access.log)
192.168.1.10 – [15/Apr/2024:10:23:45 +0800] "GET /api/v1/user/123 HTTP/1.1" 200 189 "-" "curl/7.68.0" "key123" "/internal/v1/user?id=123"
192.168.1.20 – [15/Apr/2024:10:24:10 +0800] "GET /api/v1/user/456 HTTP/1.1" 403 28 "-" "PostmanRuntime/7.32.3" "invalid_key" ""
✅ 这个方案实现了:
- 安全认证(API Key)
- 路径重写(解耦前后端)
- 访问审计(日志记录)
- 高性能(map + 无正则循环)
七、高级用法六:动态重写 + Lua 扩展(OpenResty)🚀
虽然 ngx_http_rewrite_module 功能强大,但它不支持复杂逻辑,如数据库查询、HTTP 调用、加密解密等。这时,你可以引入 OpenResty(Nginx + LuaJIT)。
💡 OpenResty 不是 Nginx 的“插件”,而是基于 Nginx 的完整 Web 平台,内置 Lua 解释器。
📌 场景:根据用户 Session ID 从 Redis 获取权限,动态重写路径
location /protected/ {
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.status = 500
ngx.say("Redis connection failed: ", err)
return
end
— 从 Cookie 获取 session_id
local cookie = ngx.var.http_cookie
local session_id = string.match(cookie, "session_id=([^;]+)")
if not session_id then
ngx.status = 401
ngx.say("Missing session")
return
end
— 查询 Redis:session:xxx → permission_level
local perm, err = red:get("session:" .. session_id)
if not perm then
ngx.status = 403
ngx.say("Invalid session")
return
end
— 根据权限重写路径
if perm == "admin" then
ngx.req.set_uri("/admin/dashboard", false)
elseif perm == "user" then
ngx.req.set_uri("/user/profile", false)
else
ngx.status = 403
ngx.say("Insufficient permissions")
return
end
— 继续处理
}
}
🌐 了解更多:https://openresty.org 这是企业级 API 网关、微服务网关的标配技术栈。
🔗 与 Java 的联动场景
你可以让 Java 服务通过 Redis 存储用户权限,Nginx 通过 Lua 查询,实现无状态网关鉴权,Java 只负责业务,Nginx 负责流量控制。
八、高级用法七:URL 重写与缓存失效策略 🔄
重写不仅仅是“跳转”,它还能控制缓存行为。
📌 场景:版本化静态资源 + 自动缓存刷新
你使用 assets-v1.2.3.js 这种命名方式,但用户访问 /assets/js/main.js,你希望自动重写为最新版本。
# 1. 定义版本映射(由构建脚本自动生成)
map $request_uri $asset_version {
default "v1.2.3";
"/assets/js/main.js" "v1.2.3";
"/assets/css/style.css" "v1.2.3";
"/assets/images/logo.png" "v1.2.3";
}
# 2. 重写静态资源请求
location ~ ^/assets/(js|css|images)/(.*)$ {
set $target_path /assets/$1/$2-$asset_version;
try_files $target_path $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
💡 为什么用 immutable? 这是 HTTP/2 的新特性,告诉浏览器“这个资源永远不会变”,即使刷新也不重新请求,极大提升性能。
📌 场景:配合 CI/CD 实现自动缓存刷新
当你使用 Jenkins、GitLab CI 构建前端时,自动在 nginx.conf 中更新 map 块:
# build.sh
VERSION=$(git rev-parse –short HEAD)
sed -i "s/\\/assets\\/js\\/main.js\\" \\"v[0-9\\.]*\\"/\\/assets\\/js\\/main.js\\" \\"v$VERSION\\"/g" /etc/nginx/nginx.conf
nginx -t && nginx -s reload
这样,每次发布都会更新版本号,缓存自动刷新,用户无感知。
九、高级用法八:多层重写与嵌套 location 的控制流 🧩
Nginx 的 location 匹配是按优先级顺序进行的:
📌 场景:混合重写 + 嵌套 location 实现复杂路由
server {
listen 80;
server_name example.com;
# 1. 静态资源优先
location ^~ /assets/ {
root /var/www/static;
expires 1y;
}
# 2. API 路由(带版本)
location ~ ^/api/v(\\d+)/(.*)$ {
set $api_ver $1;
set $api_path $2;
# 根据版本重定向到不同后端
if ($api_ver = "1") {
proxy_pass http://api-v1:8080/$api_path;
}
if ($api_ver = "2") {
proxy_pass http://api-v2:8080/$api_path;
}
if ($api_ver = "3") {
return 410 "API v3 deprecated";
}
}
# 3. 旧版重定向
location ~ ^/old-api/(.*)$ {
rewrite ^ /api/v1/$1 permanent;
}
# 4. 默认页面
location / {
root /var/www/html;
index index.html;
}
}
📊 Mermaid 图表:Nginx 请求处理流程
渲染错误: Mermaid 渲染失败: Lexical error on line 3. Unrecognized text. …ion} B –>|精确匹配|= C[直接处理] B –>| ———————-^
✅ 这个流程图清晰展示了 rewrite 如何影响 Nginx 的处理流程。记住:last 会重新进入 location 匹配阶段,这是最常被误解的地方。
十、实战演练:构建一个完整的微服务网关 🏗️
我们来构建一个完整的微服务网关,支持:
- 多版本 API 路由
- 权限验证(Header)
- 灰度发布(按用户 ID)
- 访问日志
- 静态资源分离
- 错误兜底
📁 配置文件:/etc/nginx/conf.d/gateway.conf
# === 全局变量定义 ===
map $http_x_api_key $api_access_level {
default "denied";
"key123" "admin";
"key456" "user";
"key789" "user";
}
map $http_x_user_id $feature_flag {
default "off";
"1001" "on";
"1002" "on";
"1003" "on";
}
# === 访问日志格式 ===
log_format gateway_log '$remote_addr – $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_api_key" "$http_x_user_id" "$feature_flag"';
access_log /var/log/nginx/gateway-access.log gateway_log;
server {
listen 80;
server_name gateway.example.com;
# === 静态资源 ===
location /static/ {
root /var/www/public;
expires 1y;
add_header Cache-Control "public, immutable";
}
# === API 路由 ===
location ~ ^/api/v(\\d+)/(.*)$ {
set $api_version $1;
set $api_path $2;
# 检查 API Key
if ($api_access_level = "denied") {
return 403 "Invalid API Key";
}
# 检查是否开启灰度功能
if ($feature_flag = "on") {
set $backend "backend-feature";
} else {
set $backend "backend-default";
}
# 重写路径,转发到对应后端
rewrite ^ /api/$api_version/$api_path last;
}
# === 后端服务 ===
location ~ ^/api/v(\\d+)/(.*)$ {
proxy_pass http://$backend:8080/api/$1/$2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-API-Key $http_x_api_key;
proxy_set_header X-User-ID $http_x_user_id;
proxy_set_header X-Feature-Flag $feature_flag;
}
# === 旧版重定向 ===
location ~ ^/v1/(.*)$ {
rewrite ^ /api/v1/$1 permanent;
}
# === 默认首页 ===
location / {
root /var/www/html;
index index.html;
}
# === 错误页 ===
error_page 403 /error/403.html;
error_page 404 /error/404.html;
error_page 500 502 503 504 /error/500.html;
location ~ ^/error/(.*)$ {
root /var/www/errors;
internal;
}
}
📊 Mermaid 图表:网关请求处理流程
#mermaid-svg-vQLK5wVi7pZoMjSD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vQLK5wVi7pZoMjSD .error-icon{fill:#552222;}#mermaid-svg-vQLK5wVi7pZoMjSD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vQLK5wVi7pZoMjSD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vQLK5wVi7pZoMjSD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vQLK5wVi7pZoMjSD .marker.cross{stroke:#333333;}#mermaid-svg-vQLK5wVi7pZoMjSD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vQLK5wVi7pZoMjSD p{margin:0;}#mermaid-svg-vQLK5wVi7pZoMjSD .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster-label text{fill:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster-label span{color:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster-label span p{background-color:transparent;}#mermaid-svg-vQLK5wVi7pZoMjSD .label text,#mermaid-svg-vQLK5wVi7pZoMjSD span{fill:#333;color:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD .node rect,#mermaid-svg-vQLK5wVi7pZoMjSD .node circle,#mermaid-svg-vQLK5wVi7pZoMjSD .node ellipse,#mermaid-svg-vQLK5wVi7pZoMjSD .node polygon,#mermaid-svg-vQLK5wVi7pZoMjSD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vQLK5wVi7pZoMjSD .rough-node .label text,#mermaid-svg-vQLK5wVi7pZoMjSD .node .label text,#mermaid-svg-vQLK5wVi7pZoMjSD .image-shape .label,#mermaid-svg-vQLK5wVi7pZoMjSD .icon-shape .label{text-anchor:middle;}#mermaid-svg-vQLK5wVi7pZoMjSD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vQLK5wVi7pZoMjSD .rough-node .label,#mermaid-svg-vQLK5wVi7pZoMjSD .node .label,#mermaid-svg-vQLK5wVi7pZoMjSD .image-shape .label,#mermaid-svg-vQLK5wVi7pZoMjSD .icon-shape .label{text-align:center;}#mermaid-svg-vQLK5wVi7pZoMjSD .node.clickable{cursor:pointer;}#mermaid-svg-vQLK5wVi7pZoMjSD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vQLK5wVi7pZoMjSD .arrowheadPath{fill:#333333;}#mermaid-svg-vQLK5wVi7pZoMjSD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vQLK5wVi7pZoMjSD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vQLK5wVi7pZoMjSD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vQLK5wVi7pZoMjSD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vQLK5wVi7pZoMjSD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vQLK5wVi7pZoMjSD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster text{fill:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD .cluster span{color:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vQLK5wVi7pZoMjSD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vQLK5wVi7pZoMjSD rect.text{fill:none;stroke-width:0;}#mermaid-svg-vQLK5wVi7pZoMjSD .icon-shape,#mermaid-svg-vQLK5wVi7pZoMjSD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vQLK5wVi7pZoMjSD .icon-shape p,#mermaid-svg-vQLK5wVi7pZoMjSD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vQLK5wVi7pZoMjSD .icon-shape .label rect,#mermaid-svg-vQLK5wVi7pZoMjSD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vQLK5wVi7pZoMjSD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vQLK5wVi7pZoMjSD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vQLK5wVi7pZoMjSD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
是
无效
有效
是
否
否
是
否
请求到达
是否为 /static/ ?
返回静态文件
是否匹配 /api/v\\d+/ ?
提取版本号与路径
检查 X-API-Key
返回 403
检查 X-User-ID 是否在灰度名单
设置 backend=backend-feature
设置 backend=backend-default
重写为 /api/vX/Y
转发到 backend:8080
是否为 /v1/ ?
301 重定向到 /api/v1/
返回 index.html
✅ 这个网关可以轻松扩展:
- 添加 JWT 验证
- 添加限流(limit_req)
- 添加 WAF(通过 Lua)
- 添加监控(通过 stub_status)
十一、性能优化与调试技巧 🔍
✅ 1. 测试重写规则
# 使用 nginx -t 检查语法
nginx -t
# 使用 curl 模拟请求
curl -I http://example.com/api/v1/user/123 -H "X-API-Key: key123"
# 查看响应头中的 Location(如果是 redirect)
curl -v http://example.com/old-page
✅ 2. 开启调试日志(谨慎使用)
error_log /var/log/nginx/error.log debug;
⚠️ 生产环境关闭!否则会极大影响性能。
✅ 3. 使用 echo 模块调试(需安装 ngx_http_echo_module)
location /debug {
echo "Request URI: $request_uri";
echo "Rewritten URI: $rewritten_uri";
echo "User ID: $http_x_user_id";
echo "Feature Flag: $feature_flag";
}
✅ 4. 监控重写次数
# 在日志中记录重写次数(需自定义变量)
set $rewrite_count 0;
if ($request_uri ~ "^/old") {
set $rewrite_count $rewrite_count+1;
rewrite ^ /new last;
}
实际中,建议使用 Prometheus + Nginx Exporter 监控 nginx_http_requests_total。
十二、常见错误与避坑指南 ⚠️
| rewrite 无限循环 | rewrite 后再次匹配到相同 location | 使用 break 或 last 明确终止 |
| if 嵌套太多导致性能下降 | 每个 if 都是正则匹配 | 改用 map |
| rewrite 与 proxy_pass 冲突 | 重写后路径不完整 | 使用 $uri 或 $request_uri 保留原始路径 |
| rewrite 不生效 | 没有 last 或 break | 检查 flag 是否正确 |
| map 不生效 | 变量名拼写错误 | 使用 echo 模块打印变量值 |
| rewrite 与 try_files 混用 | try_files 优先级更高 | 避免同时使用,选择其一 |
📌 经典陷阱:rewrite ^(.*)$ /index.html last; 导致死循环
location / {
rewrite ^(.*)$ /index.html last; # ❌ 死循环!
}
原因:/index.html 也会匹配 /,再次触发重写 → 无限循环 → 500 错误
✅ 正确做法:
location / {
try_files $uri $uri/ /index.html;
}
十三、总结:何时使用 rewrite?何时不用?✅
| 简单路径重定向 | rewrite … redirect/permanent |
| 静态资源版本控制 | map + try_files |
| 多语言路由 | map + rewrite |
| API 版本控制 | rewrite + proxy_pass |
| 权限控制 | map + return 403 |
| 动态路由(依赖外部系统) | OpenResty + Lua |
| 防盗链 | valid_referers + rewrite |
| 静态文件分发 | try_files |
| 请求参数重组 | map + set + rewrite |
✅ 最佳实践: 能用 map 的,不用 if;能用 try_files 的,不用 rewrite;能用 return 的,不用 rewrite。
十四、延伸阅读与推荐资源 📚
- Nginx Official Rewrite Module Documentation
- NGINX Lua Module Guide
- OpenResty 官网
- Nginx Performance Tuning
- HTTP Cache Control 最佳实践
- API Versioning Strategies
十五、结语:重写不是魔法,是艺术 🎨
Nginx 的 ngx_http_rewrite_module 不是简单的“替换字符串”,它是一套高性能、可编程、声明式的流量控制语言。当你能熟练使用 map、set、if、rewrite、try_files 的组合时,你就拥有了构建现代 Web 架构的基石能力。
无论是灰度发布、A/B 测试、多语言支持、API 网关,还是安全防护、缓存控制,URL 重写都是你手中最锋利的刀。
💬 记住: “好的重写规则,像一首诗——简洁、高效、无副作用。”
附录:Java 服务完整示例(可直接运行) 🖥️
下面是一个完整的 Spring Boot 应用,可配合上述 Nginx 配置使用:
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/internal/v1/user")
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@GetMapping
public ResponseEntity<User> getUser(
@RequestParam("id") Long userId,
@RequestHeader(value = "X-API-Key", required = false) String apiKey,
@RequestHeader(value = "X-Feature-Flag", required = false) String featureFlag) {
Set<String> validKeys = Set.of("key123", "key456", "key789");
if (apiKey != null && !validKeys.contains(apiKey)) {
return ResponseEntity.status(403).body(null);
}
User user = new User(userId,
apiKey != null ? "授权用户" : "访客",
"user@example.com");
System.out.println("✅ 请求处理: userId=" + userId + ", apiKey=" + apiKey + ", featureFlag=" + featureFlag);
return ResponseEntity.ok(user);
}
static class User {
private Long id;
private String name;
private String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
// Lombok 省略,手动写 getter
}
}
✅ 启动后访问: curl -H "X-API-Key: key123" http://localhost:8080/internal/v1/user?id=123 应返回:
{"id":123,"name":"授权用户","email":"user@example.com"}
🌟 最后:你,就是架构师
从今天起,不要再把 Nginx 当成“静态文件服务器”或“端口转发器”。 它是一个可编程的流量路由器,是你的第一道安全防线,是性能优化的黄金入口。
掌握 ngx_http_rewrite_module,你就掌握了现代 Web 架构的底层密码。
🚀 继续深入,探索 OpenResty、WAF、Rate Limiting、JWT 验证、服务网格…… 你的下一个项目,将由你亲手设计,无懈可击。
🌐 推荐阅读:https://www.nginx.com/resources/wiki/ 🌐 推荐工具:https://regex101.com —— 在线正则测试神器 🌐 推荐学习:https://www.nginx.com/blog/ —— 官方最佳实践库
感谢阅读,愿你的每一次重写,都优雅如诗。 🙏✨
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

