目录

Nginx-05 location 匹配与 rewrite

前置阅读:Nginx-04 请求处理流程与 11 个阶段

location 匹配规则和 rewrite 是 Nginx 配置中最容易出错的地方,也是面试必考。这一篇把规则讲透,并用可复现的实验验证。

1. location 的五种形式

location = /exact       { }    # 精确匹配
location ^~ /prefix     { }    # 前缀匹配,命中后不再尝试正则
location ~  \.php$      { }    # 正则匹配,区分大小写
location ~* \.(jpg|png)$ { }   # 正则匹配,不区分大小写
location /normal        { }    # 普通前缀匹配
location @name          { }    # 命名 location,只能内部跳转,不参与匹配

2. 匹配优先级(核心)

Nginx 的匹配算法不是「从上到下第一个匹配的赢」,而是有明确的优先级。完整流程:

① 先看所有「=」精确匹配
   → 命中 → 立即使用,结束匹配 ★

② 再看所有前缀匹配(包括 ^~ 和普通前缀),
   记录下「匹配长度最长」的那一个
   → 如果这个最长匹配带 ^~ → 立即使用,跳过正则 ★

③ 按配置文件中的书写顺序,依次尝试所有正则
   → 第一个匹配成功的 → 立即使用,结束匹配 ★
   (注意:正则之间是「顺序优先」,不是「最长优先」)

④ 所有正则都不匹配 → 使用第 ② 步记录的最长前缀匹配 ★

浓缩成一句话:精确 > 带 ^~ 的最长前缀 > 正则(按书写顺序)> 最长前缀

两个最容易记错的点:

  1. 前缀匹配之间比的是「长度」,正则之间比的是「顺序」。
  2. 普通前缀匹配的结果会被暂存,正则全不匹配时才启用——所以普通前缀写在正则前面还是后面都无所谓。

2.1 对照实验

server {
    listen 8080;
    default_type text/plain;

    location = / {
        return 200 "A: exact /\n";
    }
    location / {
        return 200 "B: prefix /\n";
    }
    location /docs/ {
        return 200 "C: prefix /docs/\n";
    }
    location ^~ /images/ {
        return 200 "D: ^~ /images/\n";
    }
    location ~* \.(gif|jpg|png)$ {
        return 200 "E: regex image\n";
    }
    location ~ ^/docs/.*\.html$ {
        return 200 "F: regex docs html\n";
    }
}

验证结果:

请求 命中 原因
/ A 精确匹配最高优先级
/index.html B 只有 / 前缀匹配,正则都不中
/docs/a.txt C 最长前缀 /docs/,正则不中
/docs/a.html F 正则命中,优先于最长前缀 /docs/
/images/a.png D ^~ 命中,直接跳过正则 E
/other/a.png E 正则命中
/docs/a.png E 正则 E 写在 F 前面,先匹配到 E 就结束
for u in / /index.html /docs/a.txt /docs/a.html /images/a.png /other/a.png /docs/a.png; do
    printf "%-20s -> " "$u"; curl -s "http://127.0.0.1:8080$u"
done

最后一行 /docs/a.png 命中 E 而不是 F,就是「正则按书写顺序」的直接体现。把 E 和 F 调换位置,结果就变了。

2.2 生产中最常见的一个坑

# ❌ 静态资源没被 /api/ 拦住,但 API 请求里带 .js 的会被静态规则截胡
location /api/ {
    proxy_pass http://backend;
}
location ~* \.(js|css|png)$ {
    root /var/www/static;
    expires 30d;
}

请求 /api/config.js 会命中正则规则,去 /var/www/static/api/config.js 找文件,结果 404。因为正则优先于普通前缀匹配

修复方法是给 /api/^~

# ✅
location ^~ /api/ {
    proxy_pass http://backend;
}
location ~* \.(js|css|png)$ {
    root /var/www/static;
    expires 30d;
}

规律:只要一个前缀 location 里配了 proxy_pass,并且同时存在按扩展名匹配的正则 location,就应该给这个前缀加 ^~

2.3 匹配用的是什么

匹配用的是 $uri,即:

  • 已 URL 解码/a%20b 匹配的是 /a b
  • 已规范化/a/./b/a//b/a/c/../b 都会被归一成 /a/b
  • 不含查询串/a?x=1 匹配的是 /a

这点很重要——想靠 location ~ /\.\. 防路径穿越是没用的,Nginx 早就把 .. 归一化掉了。

3. proxy_pass 的斜杠玄学

proxy_pass 的 URL 末尾有没有斜杠,行为完全不同。这是最高频的踩坑点。

# 情况一:proxy_pass 不带路径(只有 host:port)
# → 完整的原始 URI 直接拼到后面
location /api/ {
    proxy_pass http://backend;
}
# /api/user/1  →  http://backend/api/user/1

# 情况二:proxy_pass 带路径且以 / 结尾
# → location 匹配的部分被「替换」成 proxy_pass 的路径
location /api/ {
    proxy_pass http://backend/;
}
# /api/user/1  →  http://backend/user/1     ← /api/ 被去掉了

# 情况三:proxy_pass 带路径且不以 / 结尾
# → location 匹配的部分被替换成这个路径(直接字符串拼接)
location /api/ {
    proxy_pass http://backend/v1;
}
# /api/user/1  →  http://backend/v1user/1   ← ⚠️ 注意粘在一起了!

# 情况四:都带 /
location /api/ {
    proxy_pass http://backend/v1/;
}
# /api/user/1  →  http://backend/v1/user/1  ← ✅ 这才是想要的

记忆口诀:proxy_pass 后面有 URI(哪怕只是一个 /)就做「替换」,没有 URI 就做「透传」。

对照表:

location proxy_pass 请求 /api/user/1 转发到
/api/ http://b /api/user/1
/api/ http://b/ /user/1
/api/ http://b/v1 /v1user/1 ⚠️
/api/ http://b/v1/ /v1/user/1
/api http://b/ /user/1(前面多个 /?不,是 /user/1

3.1 两个特殊情况

正则 location 里,proxy_pass 不能带 URI

# ❌ 报错:proxy_pass cannot have URI part in location given by regular expression
location ~ ^/api/(.*)$ {
    proxy_pass http://backend/v1/;
}

# ✅ 用捕获组自己拼
location ~ ^/api/(.*)$ {
    proxy_pass http://backend/v1/$1$is_args$args;
}

# ✅ 或者先 rewrite 再 proxy_pass
location ~ ^/api/ {
    rewrite ^/api/(.*)$ /v1/$1 break;
    proxy_pass http://backend;
}

proxy_pass 里含变量时,URI 不会自动传递

# ❌ 请求 /api/user 会转发到 http://backend/ (URI 丢了)
set $target "backend";
proxy_pass http://$target;

# ✅ 必须显式写
proxy_pass http://$target$request_uri;

含变量的 proxy_pass 还有个副作用(也是常用的技巧):它会在运行时用 resolver 解析域名,而不是启动时解析一次。见第 03 篇。

4. rewrite 指令

rewrite 正则 替换内容 [flag];

4.1 四个 flag

flag 行为
(无) 改写 $uri继续执行后面的 rewrite 指令
last 改写 $uri停止后续 rewrite,跳回 FIND_CONFIG 重新匹配 location
break 改写 $uri停止后续 rewrite,不重新匹配 location,继续在当前 location 走后面的阶段
redirect 返回 302 临时重定向给客户端
permanent 返回 301 永久重定向给客户端

前两个是内部行为(客户端不知情,URL 不变),后两个是真的让浏览器重新发一次请求。

4.2 last vs break

这是最经典的面试题。看具体例子:

server {
    location /a/ {
        rewrite ^/a/(.*)$ /b/$1 last;      # 用 last
        return 200 "from location /a/\n";  # ← 永远执行不到
    }
    location /b/ {
        return 200 "from location /b/\n";
    }
}
# 请求 /a/x → "from location /b/"
# last 触发内部重定向,重新匹配到了 /b/
server {
    location /a/ {
        rewrite ^/a/(.*)$ /b/$1 break;     # 用 break
        return 200 "from location /a/\n";
    }
    location /b/ {
        return 200 "from location /b/\n";
    }
}
# 请求 /a/x → "from location /a/"
# break 不重新匹配 location,继续在 /a/ 里往下走

实用判断标准

  • 想让改写后的 URI 走另一个 location 的配置last
  • 只改 URI、继续用当前 location 的配置(尤其是配合 proxy_pass)→ break

配合 proxy_pass 时基本都用 break

location /api/ {
    rewrite ^/api/(.*)$ /$1 break;    # 去掉 /api/ 前缀
    proxy_pass http://backend;         # 用 break 才能保住这里的 proxy_pass
}

如果这里写 last,改写后的 /xxx 会重新匹配 location,可能匹配到别的地方去。

补充:break 指令(不带 rewrite,单独用)是另一回事,它的作用是「停止当前的 rewrite 阶段处理」,效果和 rewrite ... break 的第二部分一样。

4.3 死循环防护

内部重定向最多循环 10 次

# ❌ 死循环
location / {
    rewrite ^(.*)$ /index.php$1 last;
}
# error.log: rewrite or internal redirection cycle while processing "/index.php/index.php..."
# 客户端收到 500

改成 break 或者加条件排除:

location / {
    if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php$1 break;
    }
}

4.4 rewrite 的正则与捕获

# $1 $2... 是捕获组
rewrite ^/user/(\d+)/profile$ /profile.php?uid=$1 last;

# 替换串以 http:// https:// $scheme 开头时,自动变成 302(无需写 redirect)
rewrite ^/old$ https://new.example.com/;

# 替换串里带 ? 会丢弃原有查询参数;末尾加 ? 明确丢弃
rewrite ^/a$ /b?x=1?;      # 原查询参数被丢弃
rewrite ^/a$ /b?x=1;       # 原查询参数会被追加(因为默认追加原 args)

# 用 (?i) 做不区分大小写
rewrite (?i)^/ABC$ /abc last;

关于查询参数的规则:如果替换串里不含 ?,原始的查询串会自动追加;如果含 ?,则不追加,除非再在末尾加一个 ?

4.5 return —— 优先用它

能用 return 就不要用 rewritereturn 更快(不用跑正则)、更清晰。

return 状态码 [文本或 URL];
return 状态码;
return URL;         # 只能是 http:// https:// 开头,等价于 302
# HTTP 跳 HTTPS —— 标准写法
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

# 而不是这样(慢,且容易写错)
server {
    listen 80;
    rewrite ^(.*)$ https://$host$1 permanent;   # ❌ $1 不含查询串
}

# 返回自定义内容
location = /health {
    access_log off;
    return 200 "ok\n";
}

# 返回 JSON
location = /api/status {
    default_type application/json;
    return 200 '{"status":"ok","host":"$hostname"}';
}

# 直接断开连接(Nginx 私有码)
location ~ /\.(git|svn|env) {
    return 444;
}

注意 return 301 https://$host$request_uri; 里为什么用 $request_uri 而不是 $uri$request_uri 包含查询串,$uri 不包含。用 $uri 会导致跳转后丢参数。

5. if 的陷阱

Nginx 官方文档里有一篇专门的 If is Evil

5.1 if 只在 REWRITE 阶段执行

if 是 rewrite 模块提供的,所以它在 REWRITE 阶段(第 4 阶段)执行。这导致它和 CONTENT 阶段的指令组合时行为诡异。

if 里只有两类指令能可靠工作returnrewrite。其他的都可能出问题。

5.2 典型的 if 事故

# ❌ 事故一:if 里的 add_header 会导致 location 配置被替换
location /api/ {
    add_header X-A "1";
    if ($arg_debug) {
        add_header X-B "2";      # 进入 if 后,X-A 消失了
    }
    proxy_pass http://backend;
}

原因:if 在内部实现上会创建一个匿名的嵌套 location,进入 if 就等于进入了这个新 location。而 add_header 是数组式继承——新 location 写了一条,父级的全丢。

# ❌ 事故二:两个 if 里的 proxy_pass,后面的会覆盖前面的
location / {
    if ($arg_a) { proxy_pass http://a; }
    if ($arg_b) { proxy_pass http://b; }
    # 行为完全不可预测
}
# ❌ 事故三:if 里没有 content handler,导致 404 或未定义行为
location /files/ {
    if ($request_method = POST) {
        return 405;
    }
    # 如果条件不成立,隐式的 location 里没有 root,可能读不到文件
    root /data;
}

5.3 正确的替代方案

用 map 代替 if

# ❌
location / {
    if ($http_user_agent ~* "mobile") {
        proxy_pass http://mobile_backend;
    }
    proxy_pass http://desktop_backend;
}

# ✅
map $http_user_agent $backend_pool {
    default            "desktop_backend";
    ~*mobile|android   "mobile_backend";
}
location / {
    proxy_pass http://$backend_pool;
}

用 try_files 代替文件存在性判断

# ❌ 官方明确不推荐
if (!-f $request_filename) {
    rewrite ^ /index.html last;
}

# ✅
try_files $uri $uri/ /index.html;

用 limit_except 代替方法判断

# ❌
if ($request_method = POST) { return 405; }

# ✅
location /files/ {
    limit_except GET HEAD {
        deny all;
    }
    root /data;
}

用独立 location 代替 if

# ❌
location / {
    if ($uri ~ ^/admin) { ... }
}

# ✅
location /admin { ... }
location / { ... }

5.4 if 的条件语法

如果实在要用(配合 return/rewrite 是安全的):

if ($var)                     # 变量非空且不为 "0" 则为真
if ($var = "value")           # 字符串相等
if ($var != "value")          # 不相等
if ($var ~  "regex")          # 正则匹配,区分大小写
if ($var ~* "regex")          # 正则匹配,不区分大小写
if ($var !~ "regex")          # 正则不匹配
if (-f /path/file)            # 文件存在
if (!-f /path/file)           # 文件不存在
if (-d /path/dir)             # 目录存在
if (-e /path)                 # 文件或目录存在
if (-x /path/file)            # 文件可执行

Nginx 的 if 不支持 &&||else if。多条件只能用 map 拼变量:

# 想表达「是 POST 且带 token」
map "$request_method:$http_x_token" $need_check {
    default          0;
    "~^POST:.+$"     1;
}
if ($need_check) { ... }

6. error_page

server {
    # 基本用法:内部重定向到指定 URI
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    # 改变响应码(= 后面跟新状态码)
    error_page 404 =200 /empty.gif;      # 404 时返回 200 和一张空图
    error_page 403 =404 /404.html;       # 隐藏 403,对外表现为 404

    # 用 = 不跟状态码:使用被重定向到的那个 URI 的真实状态码
    error_page 404 = /fallback;

    # 跳转到外部地址(会变成 302)
    error_page 404 http://cdn.example.com/404.html;

    # 交给命名 location
    error_page 502 504 = @fallback;

    location = /404.html {
        root /usr/share/nginx/html;
        internal;                # 不允许客户端直接访问
    }

    location @fallback {
        proxy_pass http://backup_server;
    }
}

6.1 拦截上游的错误页

默认情况下,上游返回的 4xx/5xx 会原样透传给客户端,error_page 不生效。想让 Nginx 接管,要加 proxy_intercept_errors

location /api/ {
    proxy_pass http://backend;
    proxy_intercept_errors on;       # 让 error_page 能处理上游的错误码
    error_page 500 502 503 504 /50x.html;
}

对应的还有 fastcgi_intercept_errorsuwsgi_intercept_errors

6.2 API 服务应该关掉 error_page

对纯 API 服务,返回 HTML 错误页是灾难(客户端要解析 JSON 却收到 HTML)。应该统一返回 JSON:

location /api/ {
    proxy_pass http://backend;

    error_page 502 503 504 = @api_error;
}

location @api_error {
    default_type application/json;
    return 502 '{"code":502,"message":"upstream unavailable","request_id":"$request_id"}';
}

7. 一个完整的实战配置

综合上面所有知识点,一个典型的「前后端分离 + 静态资源」站点:

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    root /var/www/dist;

    include snippets/ssl-common.conf;
    include snippets/security-headers.conf;

    # 1. 健康检查:精确匹配,最快,不记日志
    location = /health {
        access_log off;
        return 200 "ok\n";
    }

    # 2. API 反代:用 ^~ 避免被下面的静态正则截胡
    location ^~ /api/ {
        include snippets/proxy-headers.conf;
        proxy_pass http://backend/;        # 注意结尾的 /,去掉 /api/ 前缀
        proxy_intercept_errors on;
        error_page 502 503 504 = @api_error;
    }

    # 3. WebSocket
    location ^~ /ws/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;         # 长连接要放宽
    }

    # 4. 带 hash 的构建产物:强缓存
    location ~* \.[0-9a-f]{8,}\.(js|css|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # 5. 其他静态资源
    location ~* \.(png|jpe?g|gif|svg|ico|webp)$ {
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
    }

    # 6. 屏蔽敏感路径
    location ~ /\.(git|svn|env|htaccess) {
        return 444;
    }

    # 7. SPA 兜底:所有前端路由都回 index.html
    location / {
        try_files $uri $uri/ /index.html;
        # index.html 本身不能缓存,否则发版后用户拿不到新版
        location = /index.html {
            add_header Cache-Control "no-cache, must-revalidate";
        }
    }

    location @api_error {
        default_type application/json;
        return 502 '{"code":502,"message":"service unavailable"}';
    }
}

注意第 7 条里的嵌套 locationlocation 可以嵌套在 location 里,内层的匹配优先级更高。这是给 SPA 的 index.html 单独设缓存策略的干净写法。

8. 调试技巧

用 return 把变量打出来,比看日志快得多:

location = /debug {
    default_type text/plain;
    return 200 "uri=$uri
request_uri=$request_uri
args=$args
host=$host
http_host=$http_host
remote_addr=$remote_addr
scheme=$scheme
document_root=$document_root
request_filename=$request_filename
";
}
curl "http://127.0.0.1/debug?a=1&b=2"

在日志里加上命中的 location(用 set 变量标记):

log_format debug '$remote_addr "$request" $status loc=$matched_loc';

location ^~ /api/ {
    set $matched_loc "api";
    proxy_pass http://backend;
}
location / {
    set $matched_loc "root";
    try_files $uri /index.html;
}

error_log debug 直接看匹配过程(需要 --with-debug):

[debug] test location: "/"
[debug] test location: "api/"
[debug] test location: "health"
[debug] using configuration "^~ /api/"

9. 面试题

Q:location 的匹配优先级是什么?

= 精确匹配 > ^~ 前缀匹配(取最长的) > 正则匹配(~/~*,按配置文件书写顺序,第一个匹配的赢) > 普通前缀匹配(取最长的)。注意:前缀匹配之间比长度,正则之间比顺序;普通前缀的匹配结果会先暂存,只有所有正则都不匹配时才启用。

Q:^~ 的作用是什么?

它是前缀匹配修饰符,含义是「如果这个前缀是最长匹配,就直接使用它,不再尝试任何正则 location」。典型用途是保护 proxy_pass 的 location 不被按扩展名匹配的正则截胡,比如 /api/config.js 不应该被 location ~* \.js$ 抢走。

Q:proxy_pass http://b;proxy_pass http://b/; 有什么区别?

前者不带 URI,原始请求 URI 完整透传(/api/x/api/x);后者带 URI(一个 / 也算),会把 location 匹配的部分替换掉(location /api//api/x/x)。规律是「有 URI 就替换,没 URI 就透传」。

Q:rewrite ... lastrewrite ... break 的区别?

last 停止执行后续 rewrite,并触发内部重定向,跳回 FIND_CONFIG 阶段重新匹配 location(最多 10 次);break 停止执行后续 rewrite,但不重新匹配 location,继续在当前 location 的配置下走后面的阶段。配合 proxy_pass 改写路径时必须用 break,否则会跳到别的 location。

Q:为什么官方说 “If is Evil”?

if 在实现上会创建一个匿名的嵌套 location,进入 if 相当于切换了配置上下文。这导致:(1) add_header/proxy_set_header 这类数组式指令的父级配置全部丢失;(2) 多个 if 里写 proxy_pass 行为不可预测;(3) if 里缺少 content handler 时可能 404。只有 returnrewriteif 里是安全的。替代方案:maptry_fileslimit_except、拆成独立 location。

Q:Nginx 的 if 支持 && 吗?

不支持,也不支持 ||else if。多条件判断的标准做法是用 map 把多个变量拼成一个 key 再映射,或者用嵌套 if(但要小心上下文问题)。

Q:return 301 https://$host$request_uri 里为什么不用 $uri

$uri 不包含查询串,用它会导致跳转后丢失 ?a=1 这样的参数。$request_uri 是客户端原始请求的完整 URI,包含查询串。

Q:上游返回 502,我配的 error_page 502 /50x.html 为什么不生效?

默认情况下上游的错误响应会原样透传,需要显式打开 proxy_intercept_errors on; 才会交给 error_page 处理。


上一篇:Nginx-04 请求处理流程与 11 个阶段 | 下一篇:Nginx-06 反向代理与负载均衡