强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Nginx 从入门到精通 / 04 - Location 路由匹配 / Location Matching

Location 路由匹配 / Location Matching

🟢 基础 / Basics — Location 语法

四种匹配前缀

# 1. 精确匹配 (=)
location = /login {
    # 只匹配 /login,不匹配 /login/ 或 /login/page
}

# 2. 前缀匹配 (^~) — 优先于正则
location ^~ /static/ {
    # 匹配以 /static/ 开头的路径
    # 一旦匹配,不再检查正则
}

# 3. 正则匹配 (~ 区分大小写, ~* 不区分大小写)
location ~* \.(jpg|png|gif)$ {
    # 匹配图片文件(不区分大小写)
}

# 4. 普通前缀匹配(无修饰符)
location /api/ {
    # 匹配以 /api/ 开头的所有路径
}

匹配优先级(最重要!)

请求: GET /static/js/app.js

优先级顺序(从高到低):

1. 精确匹配    = /static/js/app.js     → 精确命中则直接使用,停止搜索
2. 前缀匹配    ^~ /static/             → 命中最长前缀后,停止搜索(不查正则)
3. 正则匹配    ~ \.js$                 → 按配置文件中的顺序,第一个匹配的生效
4. 普通前缀匹配  /static/              → 命中最长前缀,但继续搜索正则
5. 默认         /                      → 兜底

记忆口诀 / Mnemonic:

= (精确)  >  ^~ (前缀优先)  >  ~ (正则顺序)  >  / (前缀最长)

优先级实战演示

server {
    location = / {
        return 200 "精确匹配: 只匹配根路径";
    }

    location / {
        return 200 "普通前缀: 兜底";
    }

    location /doc {
        return 200 "普通前缀: /doc";
    }

    location ^~ /doc/ {
        return 200 "前缀优先: /doc/";
    }

    location ~* \.html$ {
        return 200 "正则: .html 文件";
    }
}
请求                    命中的 location         原因
/                       = /                     精确匹配
/index.html             ~* \.html$              正则匹配(/ 兜底被正则覆盖)
/doc                    /doc                    最长前缀匹配
/doc/                   ^~ /doc/                ^~ 前缀优先,停止正则搜索
/doc/page.html          ^~ /doc/                ^~ 前缀优先,即使正则能匹配
/about                  /                       兜底前缀匹配
/about.html             ~* \.html$              正则匹配

🟡 进阶 / Intermediate — Rewrite 与正则捕获

rewrite 指令

# 语法: rewrite regex replacement [flag];

# flag 说明:
# last     — 停止当前 rewrite,重新发起 location 匹配(类似 continue)
# break    — 停止当前 rewrite,直接在当前 location 执行(不再重新匹配)
# redirect — 返回 302 临时重定向
# permanent — 返回 301 永久重定向

rewrite 实战

server {
    server_name example.com;

    # 1. 永久重定向:旧路径 → 新路径
    rewrite ^/old-page$ /new-page permanent;         # 301

    # 2. 临时重定向
    rewrite ^/maintenance$ /coming-soon redirect;     # 302

    # 3. 正则捕获($1, $2...)
    rewrite ^/user/(\d+)$ /profile?id=$1 last;

    # 4. 带条件的重写
    rewrite ^/blog/(.*)$ /articles/$1 last;

    # 5. 非 URL 的 rewrite(只改参数)
    if ($request_uri ~* "^/search\?q=(.*)") {
        rewrite ^ /search?q=$1&lang=zh last;
    }

    location /profile {
        # 这里会收到重写后的请求 /profile?id=123
        proxy_pass http://backend;
    }

    location /articles {
        proxy_pass http://backend;
    }
}

rewrite 的 last vs break 深入对比

# 场景 A:last — 重新匹配 location
location /old/ {
    rewrite ^/old/(.*)$ /new/$1 last;
    # ↑ last: 跳出当前 location,重新从头匹配
    # 请求会进入下面的 location /new/
}

location /new/ {
    proxy_pass http://backend;
}

# 场景 B:break — 在当前 location 执行
location /static/ {
    rewrite ^/static/(.*)$ /assets/$1 break;
    # ↑ break: 不跳出,直接用 rewrite 后的 URI 在当前 location 执行
    root /var/www;    # 最终文件: /var/www/assets/xxx
}
last 执行流程:
/old/page → 匹配 location /old/ → rewrite → /new/page
           → 重新匹配 → 命中 location /new/ → proxy_pass

break 执行流程:
/static/app.js → 匹配 location /static/ → rewrite → /assets/app.js
               → 不重新匹配 → root /var/www → 文件: /var/www/assets/app.js

if 指令(慎用!)

# ⚠️ Nginx 官方不推荐在 location 中使用 if
# 但在 server 块中做简单条件判断还是可以的

# ✅ 推荐用法:

# 强制 HTTPS
if ($scheme = http) {
    return 301 https://$host$request_uri;
}

# www → 裸域
if ($host = www.example.com) {
    return 301 https://example.com$request_uri;
}

# 禁止特定 User-Agent
if ($http_user_agent ~* (curl|wget|python)) {
    return 403;
}
# ❌ 避免的用法(容易出 bug):

# if 内使用 proxy_pass(行为不可预期)
location /api/ {
    if ($arg_debug) {
        proxy_pass http://debug-backend;   # ⚠️ 可能不生效
    }
    proxy_pass http://production-backend;
}

# 更好的写法:用 map 或 split_clients

map 指令(变量映射)

http {
    # 根据 User-Agent 映射变量
    map $http_user_agent $is_bot {
        default         0;
        ~*(googlebot|bingbot|yahoobot)  1;
        ~*(crawler|spider|bot)          1;
    }

    # 根据请求路径映射后端
    map $uri $backend {
        default         http://127.0.0.1:3000;
        /api/v1/        http://127.0.0.1:3001;
        /api/v2/        http://127.0.0.1:3002;
        /admin/         http://127.0.0.1:3003;
    }

    # 根据客户端 IP 映射限流区域
    map $binary_remote_addr $rate_limit_zone {
        default         normal;
        10.0.0.0/8      internal;    # 内网不限流
        172.16.0.0/12   internal;
    }

    server {
        location / {
            proxy_pass $backend;     # 根据路径动态选择后端

            # 机器人限流
            if ($is_bot) {
                return 429;
            }
        }
    }
}

正则性能提示

# ❌ 性能差:在每个 location 都用复杂正则
location ~ "^/api/v[0-9]+/(users|posts|comments)/[0-9]+$" {
    # ...
}

# ✅ 性能好:先用前缀匹配缩小范围
location /api/ {
    # 先前缀匹配 /api/
    
    # 再在 /api/ 内部做细分
    location ~ ^/api/v(\d+)/(users|posts|comments)/(\d+)$ {
        set $api_version $1;
        set $resource $2;
        set $id $3;
        proxy_pass http://backend_v$api_version;
    }
}

🔴 高级 / Advanced — 内部重定向与嵌套 Location

内部重定向指令对比

# rewrite ... last
# 跳出当前 location,重新进行完整的 location 匹配
# 可以跳到任何 location

# rewrite ... break
# 留在当前 location,使用 rewrite 后的 URI
# 不重新匹配

# try_files
# 按顺序检查文件,最后一个参数可以是 URI 或状态码
# 内部重定向

# error_page
# 可以将错误码重定向到另一个 URI
# 内部重定向

嵌套 Location 的作用域

location /api/ {
    # 父级配置
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # 子 location 继承父级配置,可以覆盖
    location /api/v1/ {
        proxy_pass http://v1-backend;
        # 继承 proxy_set_header
    }

    location /api/v2/ {
        proxy_pass http://v2-backend;
        proxy_set_header X-API-Version "2";   # 覆盖/新增 header
    }

    # 正则 location 不能嵌套(有 workaround)
    location ~ ^/api/v(\d+)/uploads {
        # ⚠️ 嵌套正则 location 可能导致意外行为
        # 建议用 map 替代
        client_max_body_size 100m;
        proxy_pass http://upload-backend;
    }
}

# ⚠️ 命名 location 只能被 error_page / try_files / internal 引用
location @fallback {
    proxy_pass http://fallback-backend;
}

location / {
    try_files $uri $uri/ @fallback;   # ✅ 正确用法
}

经典 SPA + API 分离配置

server {
    listen 443 ssl;
    server_name app.example.com;
    root /var/www/spa;

    # API 请求转发到后端
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 静态资源(带 hash 的文件名,长期缓存)
    location ~* \.(css|js|jpg|png|svg|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # SPA 路由:其他所有请求返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
        # 例如请求 /dashboard/settings → 返回 index.html
        # 由前端路由(React Router / Vue Router)处理
    }

    # 禁止访问隐藏文件
    location ~ /\. {
        deny all;
    }
}

多级 Rewrite 实战:短链服务

server {
    server_name s.example.com;

    # 短链映射
    location / {
        # 用 map 或 Lua 查询数据库获取长链接
        # 这里演示纯 Nginx 的方式

        # 规则 1:/g/google → https://www.google.com
        rewrite ^/g/(.+)$ https://www.google.com/search?q=$1 permanent;

        # 规则 2:/r/123 → /redirect?id=123(后端处理)
        rewrite ^/r/(\d+)$ /redirect?id=$1 last;

        # 规则 3:/gh/user/repo → GitHub 仓库
        rewrite ^/gh/([^/]+/[^/]+)$ https://github.com/$1 permanent;

        # 兜底
        return 404 "Short link not found";
    }

    location /redirect {
        proxy_pass http://127.0.0.1:3000;
    }
}

配置调试技巧

# 1. 开启调试日志(需要编译 --with-debug)
# /etc/nginx/nginx.conf
error_log /var/log/nginx/error.log debug;

# 2. 查看 Nginx 的 location 匹配过程
# 在 error.log 中会看到:
# *1 using configuration "/api/v1/users"
# 这说明 Nginx 选择了哪个 location

# 3. 用 curl 测试不同路径
curl -I http://localhost/test
curl -I http://localhost/api/v1/users
curl -I http://localhost/static/file.js

# 4. 添加调试 header
add_header X-Debug-Location "matched" always;

小结 / Summary

层级你需要知道的 / What You Need to Know
🟢 基础四种前缀(=, ^~, ~, /),优先级顺序
🟡 进阶rewrite(last vs break),if,map,正则捕获
🔴 高级内部重定向,嵌套 location,SPA + API 配置,调试技巧

下一章:反向代理 / Reverse Proxy