nginx 会先处理正则 location 再处理前缀匹配吗?
不会——前缀 location 先检查,但只要有正则匹配,胜出的仍是正则。 nginx 先检查所有前缀 location 并记住最长的那个匹配,然后按正则在文件里出现的顺序求值。第一条匹配的正则胜出。如果没有正则匹配,就使用之前记下的前缀 location。唯一的例外是 ^~:当最长匹配前缀带着它时,整个正则阶段会被跳过。
看清哪个 nginx location 块生效,其他块各自输在哪一步。免费在线 location 匹配规则与优先级测试器,支持 =、^~、~、~*,全程浏览器内运行。
~* \.(gif|jpg|jpeg)$ 配置顺序中第一条匹配的正则。长度无关紧要。
/documents/1.jpg/documents/1.jpg—匹配只跑在规范化后的路径上。查询字符串会被最先切走,全程不参与。
| 行号 | location | 阶段 | 结果 | 原因 |
|---|---|---|---|---|
| 2 | = / | 精确 | 未匹配 | = location 要求整个 URI 相等,而不是以它开头。 |
| 3 | / | 前缀 | 匹配但更短 | 它匹配上了,但另一个前缀匹配了更多字符。 |
| 4 | /documents/ | 前缀 | 匹配但更短 | 它匹配上了,但另一个前缀匹配了更多字符。 |
| 5 | ^~ /images/ | 前缀 | 未匹配 | URI 不以这个前缀开头。 |
| 6 | ~* \.(gif|jpg|jpeg)$ | 正则 | 已选中 | 配置顺序中第一条匹配的正则。长度无关紧要。 |
| 修饰符 | 写法 | 匹配依据 | 顺序 | 阻止正则 | 典型用途 |
|---|---|---|---|---|---|
| = | location = /path | 相等 | 1 | 是 | / 这类高频路径——最快的匹配方式。 |
| ^~ | location ^~ /path | 以此开头 | 2 | 是 | 绝不能交给正则的目录,比如上传目录。 |
| ~ | location ~ regex | PCRE 正则 | 3 | 否 | 区分大小写的扩展名路由。 |
| ~* | location ~* regex | PCRE 正则 | 3 | 否 | 不区分大小写的扩展名路由。 |
| (无) | location /path | 以此开头 | 4 | 否 | 按路径做常规路由。 |
| @ | location @name | 仅内部使用 | — | 否 | error_page 和 try_files 的兜底目标。 |
顺序这一列不是简单的排名。前缀 location 只有在带 ^~ 且本身就是最长匹配时,才会停止正则求值——这正是 ^~ 块有时看起来什么都没做的原因。
| # | 阶段 | 结束查找 | 发生了什么 |
|---|---|---|---|
| 1 | 规范化 | 否 | 百分号解码,解析 . 和 ..,合并重复斜杠。查询字符串在这里被切走,全程不参与匹配。 |
| 2 | 精确 | 是 | location = /path,按相等比较。命中即立刻结束查找。 |
| 3 | 自动重定向 | 是 | 名字正好是 URI 加一个斜杠的代理 location。nginx 返回 301,根本走不到正则。 |
| 4 | 前缀 | 否 | 所有匹配的前缀都会被比较,最长的被记住。配置顺序被忽略。 |
| 5 | 嵌套 | 否 | 进入胜出的前缀。嵌套的正则先于父层被尝试。 |
| 6 | 正则 | 是 | 正则按配置顺序尝试,第一条匹配的胜出。写在后面的、更长或更具体的正则永远不会跑。 |
| 7 | 兜底 | 是 | 没有正则匹配,于是使用之前记住的前缀。 |
本页的选择规则是对着一台真实运行的 nginx 1.27.5 验证过的,而不是抄自二手文章;引擎也由这些实跑结果推导出的单元测试覆盖。
不会——前缀 location 先检查,但只要有正则匹配,胜出的仍是正则。 nginx 先检查所有前缀 location 并记住最长的那个匹配,然后按正则在文件里出现的顺序求值。第一条匹配的正则胜出。如果没有正则匹配,就使用之前记下的前缀 location。唯一的例外是 ^~:当最长匹配前缀带着它时,整个正则阶段会被跳过。
只对正则 location 重要。 前缀 location——包括 = 和 ^~——按最长匹配挑选,所以它们在文件里的顺序完全无关。正则 location 自上而下依次尝试,第一条匹配的胜出,所以挪动一个正则块就会换掉真正生效的那条。把更具体的正则放在更宽泛的下面,它永远不会执行。
它让 nginx 在前缀匹配后就停手,而不是提高优先级。 如果最长匹配的前缀 location 带 ^~,nginx 会跳过正则阶段并使用这个块。它不会让前缀匹配变得更长,也不会把它排在其他前缀之上——它只是压制正则求值。这就是为什么当另一个更长的普通前缀也匹配时,^~ 块看起来什么都没做:被记住的是更长的那个,而它不压制任何东西。
不一样——/static 连 /staticfoo 也会匹配。 前缀匹配就是普通的字符串比较,不以路径段为边界,所以 location /static 也会伺候 /staticfiles 和 /static-backup。另外,当 /static/ 配合 proxy_pass 使用时,对不带尾斜杠的 /static 的请求,会在考虑任何正则之前先得到一个 301,重定向到 /static/。
会——参与匹配的是规范化后的 URI,不是原始请求。 nginx 在挑选 location 之前会解码 %XX、解析 . 和 ..、压缩重复斜杠。解码出的 %2F 变成真正的分隔符并参与这一步解析,所以 /a/b%2F..%2Fzz 是按 /a/zz 去匹配的。查询字符串会被最先切走,全程不参与匹配。
location 块告诉 nginx:当请求的 URI 匹配某个模式时该做什么。一个 server 块里通常有好几个,而真正有意思的不是每个块做什么,而是 nginx 会挑中哪一个——因为选择规则并不是大多数人以为的那样。
一共有五种写法。location = /path 只在整个 URI 完全相等时匹配。location /path 匹配任何以这些字符开头的 URI。location ^~ /path 是同样的前缀比较,外加一个额外效果。location ~ regex 和 location ~* regex 应用 PCRE 模式,分别区分和不区分大小写。location @name 完全不参与 URI 匹配,只作为 try_files 和 error_page 的跳转目标存在。
选择过程分阶段进行。首先 URI 被规范化:百分号解码,解析 . 和 ..,合并重复斜杠,切走查询字符串。接着,若有 = location 与 URI 相等,查找立即结束。然后比较所有匹配的前缀,记住最长的那个——配置顺序在这一步完全不起作用。如果记住的那个前缀带 ^~,nginx 就停下并使用它。否则按正则在文件里出现的顺序依次尝试,第一条匹配的胜出,排在后面的那条再具体也没用。都不匹配,就用之前记住的前缀。
这里面有两条规则方向相反,困惑就出在这儿:前缀按长度选、与顺序无关,正则按顺序选、与长度无关。一份从上往下读起来完全正确的配置,仍然可能把请求路由到你没打算去的地方,而且再读多少遍也看不出来。本页会拿你自己的配置把整个流程重放一遍,标出每个块是在哪一步出局的。
# From the nginx documentation. Which block serves each request?
server {
location = / { } # A
location / { } # B
location /documents/ { } # C
location ^~ /images/ { } # D
location ~* \.(gif|jpg|jpeg)$ { } # E
}
# / -> A exact match, search ends here
# /index.html -> B no regex matched, longest prefix used
# /documents/document.html -> C longer prefix than B
# /images/1.gif -> D ^~ won the prefix stage, regex skipped
# /documents/1.jpg -> E regex beats the longer prefix C
# The last two lines are the whole lesson: identical-looking prefixes
# behave differently because only one of them carries ^~. 你写的每个 location 都占一行:匹配了但更短、被 ^~ 前缀跳过、因为更早的正则已匹配而不可达,或者干脆没匹配上。真正能解决问题的,往往是知道其他块为什么输。
规范化、精确匹配、最长前缀记忆、^~ 短路、按文件顺序的正则以及兜底,都作为离散步骤对着你自己的配置展示,而不是抽象地讲一遍。
当 ^~ 前缀压制了正则阶段时,每条被跳过的正则都会被标为「已跳过」,而不是无声消失;当 ^~ 因为更长的普通前缀胜出而失效时,也会一并显示。
nginx 会进入胜出的前缀并在其子块中查找,所以嵌套的正则先于父层执行。表格里嵌套块保留缩进,结构一眼可读。
浏览器跑的是 ECMAScript 正则,不是 PCRE。原子组、占有量词、POSIX 字符类以及 \A、\K 这类转义会被标记,而不是被悄悄算错,绝不会把一个自信的错误答案当成事实端上来。
服务器配置里带着上游主机名、内部端口和鉴权规则。解析是零依赖的普通字符串处理,没有网络调用,并由每次构建都会跑的自动化契约测试验证。
把完全展开后的配置打印出来,用来确认到底加载了什么非常有用。但它不会告诉你某个 URI 会选中哪个 location——这正是本页要补的缺口。
能拿到的最权威答案:"using configuration" 那一行写着 nginx 真正选中的块。代价是需要 root、需要 reload、还需要一台你能访问的服务器——所以它是在部署之后回答,而不是之前。
擅长整文件的 lint 和安全规则。它们一般在服务端运行,也就意味着要把含内部主机名和证书路径的配置上传上去。
nginx 官方文档把算法写得很准确,值得认真读一遍。问题在于拿八个块和一个 URI 手工套用时容易出错,因为其中两条规则的方向是相反的。
location /documents/ · location ~* \.(gif|jpg|jpeg)$ · GET /documents/1.jpg
~* \.(gif|jpg|jpeg)$ wins
前缀 /documents/ 匹配成功,而且是文件里最长的前缀,nginx 会先把它记下来——然后照样去求值正则,并把请求交给第一条匹配的正则。长度输给了正则阶段。如果那个前缀写成 ^~ /documents/,正则就根本不会跑。这是 nginx 官方文档里的例子,也是最值得记住的一个。
location /uploads/ · location ~ \.php$ { fastcgi_pass ... } · GET /uploads/evil.php ~ \.php$ wins — the upload lands in the interpreter
前缀 /uploads/ 匹配上了,但普通前缀不会阻止正则阶段,于是别人上传的文件被直接交给 PHP-FPM。写成 location ^~ /uploads/ 会短路正则阶段,把这个口子堵上。这不是假设:一长串「上传变 RCE」的漏洞报告都是这个形状,而有洞和安全之间只差两个字符。
location ^~ /a/ · location /a/b/ · location ~ \.php$ · GET /a/b/x.php
~ \.php$ wins — the ^~ never applied
^~ 只有在它本身就是最长匹配前缀时,才会压制正则阶段。这里 /a/b/ 更长,所以被 nginx 记住的是它,而它是普通修饰符,正则阶段照跑。那个 ^~ 块还在文件里,看上去仍然像在做防护,对这个请求却毫无作用。从上往下读配置发现不了这一点;比较前缀长度才能。
location /static · location /static/ · GET /staticfoo
/static wins
前缀匹配比的是字符,不是路径段。/staticfoo 以 /static 开头,所以匹配成功;而 /static/ 完全匹配不上,因为 URI 在那个位置上没有斜杠。任何只是开头字母相同的路径,都会落到这个块里——一条 /static 规则最后去伺候 /static-backup,就是这么来的。
location ~ ^/a · location ~ ^/a/b/c$ · GET /a/b/c
~ ^/a wins; ~ ^/a/b/c$ is unreachable
正则按它们在文件里出现的顺序求值,第一条匹配就结束查找。第二个块更具体,而且和这个 URI 精确吻合,但它永远不会跑——对任何请求都不会。前缀 location 按长度选,与顺序无关;正则 location 按顺序选,与具体程度无关。把这两条规则搞混,是有人整理完文件后规则「突然不灵了」的常见原因。
location /a/ · location /b/ · GET /a/b%2F..%2Fzz
$uri becomes /a/zz, so /a/ wins
nginx 会在查找任何 location 之前先做百分号解码、解析 . 和 ..、并合并重复斜杠。%2F 解码成真正的分隔符,随后参与这一步解析,所以这个目标并不会停在 /a/b/ 里面——它落到了 /a/zz。拿你输入的原始目标而不是规范化后的 $uri 去匹配,在这里会得出错误答案。
整段 server 块可以,只放你正在琢磨的那几个 location 块也可以。嵌套 location 能被识别,原始行号会保留,表格和你的文件对得上。
按它在链路上真实到达的样子输入路径,包括百分号编码和查询字符串。表格上方那三行会显示它在匹配前是怎么被规范化的。
结论卡片写出被选中的块和一行原因。下面的判定表解释其余每一个块:它在哪一步被淘汰,为什么。
未加锚点的正则、少了尾斜杠的前缀、写了正则模式的 ^~、不可达的重复项,以及能被 PHP 正则接管的上传目录,都会被报出来。
复制链接会把配置和 URI 编码进 URL 片段,同事打开看到的就是你眼前这一份。片段永远不会传给服务器。
^~ 只在已经按长度胜出的那个前缀上被检查。更长的普通前缀先赢,随后正则阶段照跑,就像 ^~ 不存在一样。
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
# /a/b/x.php -> ~ \.php$ location ^~ /a/ { }
location ^~ /a/b/ { }
location ~ \.php$ { }
# /a/b/x.php -> ^~ /a/b/ 前缀匹配比的是字符而不是路径段,所以这个块也会伺候所有以相同字母开头的兄弟路径。
location /static { root /var/www; }
# also serves /staticfoo and /static-backup location /static/ { root /var/www; }
location = /static { return 301 /static/; } 正则按文件顺序尝试,第一条匹配就结束查找,所以更精确的那个块对任何请求都不会执行。
location ~ ^/api { }
location ~ ^/api/v2/users$ { }
# the second is unreachable location ~ ^/api/v2/users$ { }
location ~ ^/api { } ^~ 接的是字面前缀。nginx 会毫无怨言地加载这个文件,而这个块只是永远匹配不上任何东西,评审时很难发现。
location ^~ "\.php$" { deny all; } location ~ \.php$ { deny all; } 查询字符串在规范化阶段就被切走,所以 location 永远不可能靠它匹配。改成在块里读 $arg_name。
location /search?q= { }
# never matches anything location /search {
if ($arg_q = "") { return 400; }
} . 和 .. 段被解析,重复斜杠被合并,然后才开始挑 location。解码出的 %2F 变成真正的分隔符并参与这一步解析,所以 /a/b%2F..%2Fzz 是按 /a/zz 匹配的。有三个解码字符是例外,会保持字面:%25、%23 和 %3F——这就是 /a%3Fx=1 路径里带问号、查询字符串却为空的原因。这里的 + 不是空格,只有 %20 才是。爬到根目录之上的穿越和非法转义,都会在匹配开始前被 400 拒掉。/static 匹配 /staticfoo。胜出者只是被记住而不是立刻使用,因为正则阶段仍可能把它覆盖掉。^~ 块对这个请求毫无作用。把 ^~ 放在嵌套 location 上,挡不住在外层声明的正则;它也从不压制嵌套在自己块内部的正则。^ 只锚定 URI 的开头。有一处差异带安全后果:PCRE 的 $ 也匹配紧邻结尾换行之前的位置,所以以 %0A 结尾的 URI 仍然满足 \.php$,尽管 JavaScript 引擎会拒绝。本页模拟并报告这个行为,因为它是绕过按扩展名设定的规则的已知手法。location ^~ /uploads/,好让正则阶段没机会把存进去的文件交给解释器。普通前缀看起来等价,其实不是。location /static/ 而不是 location /static,除非你就是想让 /staticfoo 和 /static-backup 也走同一个块。裸路径也需要处理时,另加一条 location = /static。location ~ /admin 会在 URI 的任意位置搜索,也会匹配 /public/admin/x。你要的是开头,就写 ~ ^/admin。只在结尾加锚点,对按扩展名路由来说是正常且正确的。error_log /var/log/nginx/debug.log debug;,然后找 "using configuration" 那一行,它会写出被选中的 location。两种办法回答的是不同的问题:日志告诉你线上服务器实际做了什么,这个页面告诉你还没上线的配置会做什么。 ^~,整个正则阶段被跳过了。再或者正则本身没问题,但 URI 不是你以为的那个:匹配跑在规范化之后的路径上,已经做过百分号解码和 .. 解析,查询字符串也已被移除。 = location 要求整个 URI 完全相等,而不是以这个模式开头。location = /a/ 匹配不上 /a,location = /a 也匹配不上 /a/b。尾斜杠在这里就是普通字符,所以这两种写法是不同的字符串。一旦精确 location 命中,查找立刻结束,其余的连比都不比。 location = /a 会匹配对 /a?x=/b 的请求。要按参数分支,只能在块里读 $arg_name 或 $args。有个细节值得知道:%3F 解码成一个留在路径里的字面问号,所以 /a%3Fx=1 的查询字符串是空的,而路径里含一个 ?。 (?i) 这类内联修饰符、[[:alpha:]] 这类 POSIX 字符类,以及 \A、\K 这类会被 JavaScript 当成普通字母的转义。某个 location 被标记后,其余候选仍按 nginx 的顺序求值,但那个块的结论需要到真实服务器上确认。如果你正在写模式本身,正则测试器对 ECMAScript 语法有更完整的覆盖。 location /Static/ 匹配不上 /static/x。在 macOS、Cygwin 这类大小写不敏感的文件系统上,nginx 比较前缀时不区分大小写,并且会额外让每个正则 location 都表现得像 ~*。本页模拟的是 Linux 行为,生产服务器几乎都跑在上面。如果你在 Mac 上开发、部署到 Linux,这个差异会把一条坏掉的规则一直藏到上线。 try_files 在那之后、在已经胜出的块里执行。如果请求根本没走到含 try_files 的块,这条指令就没有意义——常见原因是一条 ~ \.php$ 正则先把请求接走了,带兜底的前缀块压根没机会。之后发出的内部重定向确实会重新开始匹配,所以被重写过的 URI 会从头再走一遍 location 列表。 / 结尾的 location 带 proxy_pass 或其他 *_pass 指令,那么对同一路径不带斜杠的请求会在 location 选择阶段就得到 301——早于任何正则求值。另外,静态文件模块在路径解析到磁盘上真实存在的目录时也会发 301。前一种在这里能看到;后一种取决于你的文件系统。加一条 location = /path 可以压掉前一种。 ^~ 挡不住它自己内部嵌套的正则;而当外层的兄弟块先胜出时,嵌套还会让一个全局更长的前缀变得不可达。本页按你写的缩进还原嵌套块。 网络与 API
在浏览器中构建 curl 命令——设置请求方法、请求头、认证与请求体,即刻获得可复制的命令。支持 Bearer、POST JSON、文件上传预设。免费、隐私安全、无需注册。
网络与 API
在线生成 htpasswd 条目,支持 bcrypt、Apache MD5 (apr1)、SHA-1 等算法,并输出 Apache、nginx 和 Docker 的即用配置。100% 在浏览器中运行,密码不上传。
网络与 API
生成 Open Graph、Twitter Card 与 SEO meta 标签,并实时预览在 Google、Facebook、X 上的分享效果。100% 免费、纯浏览器运行、无需注册——一键复制代码即可粘贴使用。
网络与 API
不用再数十六进制位。免费在线 traceparent 解码器,全程在浏览器内运行,不上传任何数据。解析 trace ID、span ID、全部 8 个 trace-flags 位,校验 tracestate,并转换为 Datadog、X-Ray、B3 格式。
安全工具
在线解密 AES —— 支持 GCM/CBC/CTR、口令或原始密钥,自动识别 OpenSSL 与 CryptoJS 的 "U2FsdGVkX1" 格式。100% 浏览器本地运行,密钥永不离开页面。
安全工具
免费在线 AES 加密工具 — 支持 AES-128/192/256、GCM/CBC/CTR 模式,可用口令(PBKDF2)或原始密钥。100% 浏览器本地运行,不上传任何数据。