欢迎光临
我们一直在努力

Nginx- ngx_http_rewrite_module 模块:URL 重写高级用法

在这里插入图片描述

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕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 含义

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 需要:

  • 验证请求头 X-API-Key 是否在白名单中
  • 如果在,重写为 /internal/v1/user?id=123
  • 如果不在,返回 403
  • 同时记录访问日志到 /var/log/nginx/api-access.log
  • 📁 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/ —— 官方最佳实践库


    感谢阅读,愿你的每一次重写,都优雅如诗。 🙏✨


    🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

    赞(0)
    未经允许不得转载:171主机测评 » Nginx- ngx_http_rewrite_module 模块:URL 重写高级用法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址