拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Nginx location与proxy_pass配置详解:匹配规则、URI替换与高频场景实战

说起 Nginx 配置location和proxy_pass这两兄弟绝对是踩坑率最高的部分。很多人看官方文档的时候觉得很简单结果一上线就翻车要么代理到了错误路径要么接口 404要么配置了身份验证却死活不生效。这篇文章我打算把 Nginx 的 location 匹配规则、proxy_pass 带不带 URI 的差异以及几个高频场景多路径转发、只转发带参数的请求、反向代理到后端、给 index.php 访问加登录验证一次性讲透每一步都给出可以直接抄的配置和验证方法。适合正在调 Nginx 配置的运维、后端开发也适合想彻底搞懂反向代理原理的前端同学。看完你至少能保证一件事自己写的 location 一定匹配到自己想要的那条proxy_pass 转发出去的 URL 也是自己预期的那一个。1. 先搞清楚 location 匹配规则和 proxy_pass 的基础原理1.1 location 匹配的优先级其实是一套“游戏规则”写 location 之前优先把匹配规则理顺。因为很多人配置看不出问题是因为没意识到 Nginx 选择 location 不是“从上往下找第一个匹配”而是有自己的一套偏好。Nginx 的 location 支持五种写法精确匹配、前缀匹配空修饰符、不区分大小写的前缀匹配~和~*是正则不是前缀、强制前缀匹配^~以及普通正则匹配。其中真正参与“优先级竞争”的按顺序是精确匹配 普通前缀匹配取最长 正则匹配按书写顺序 兜底前缀匹配。我整理成了一张表方便你对照理解修饰符含义示例说明精确匹配location /api/health只有完全一致才命中命中后立即结束空普通前缀匹配location /api/所有以 /api/ 开头的请求按最长前缀择优^~强制前缀匹配location ^~ /api/命中后不再检查正则相当于“前缀一票否决”~区分大小写的正则location ~ \.php$按正则顺序匹配命中即停止~*不区分大小写的正则location ~* \.(jpg|png)$同上但不区分大小写具体匹配流程是这样的Nginx 先遍历所有普通前缀 location记下最长的那一个。如果这个最长前缀是^~写法的直接使用不再去看正则如果最长的只是普通前缀那它会先把结果记在一边继续去匹配正则。正则是按配置文件中出现的顺序逐个匹配的第一个命中的正则获胜。如果没有任何正则命中最终才会使用刚才记下的最长普通前缀。举个例子location /api/health { return 200 ok; } location ^~ /static/ { ... } location ~* \.(gif|jpg|png)$ { ... } location /api/ { proxy_pass http://backend; }请求/api/health时因为存在精确匹配直接命中第一个返回 ok。请求/static/a.png会命中^~ /static/虽然它也满足后面的图片正则但^~让 Nginx 不再检查正则。请求/api/users它没有精确匹配普通前缀里最长的是/api/然后继续检查正则都不满足最后落到/api/这条。这里有个实操心得正则 location 在配置文件里的位置非常关键因为它是“按书写顺序匹配”。如果两条正则都能命中写在前面那条永远赢。所以我习惯把更具体的正则放前面比如\.php$放\.html$前面。而当你有一批静态资源路径不想被正则干扰时直接用^~就是最简单粗暴的解决方案。1.2 proxy_pass 带不带 URI结果天壤之别这是整个 Nginx 配置里最容易出 bug 的点没有之一。proxy_pass指令后面如果写的是http://backend这种不带 URI 的写法那么 Nginx 会把原始请求 URI 原封不动地传给上游服务器如果写的是http://backend/哪怕只多了一个斜杠也代表带了 URI此时 location 中匹配到的那一段前缀会被替换成 proxy_pass 中带的路径。这句话用文字说不直观直接看对比。假设后端服务跑在 8080 端口location /api/ { proxy_pass http://127.0.0.1:8080; }请求GET /api/user?id1后端实际收到的 URI 是/api/user?id1。这和生产环境大多数需求一致我只是做一个透明转发后端就按原来的路径路由。但下面这种写法就完全不同了location /api/ { proxy_pass http://127.0.0.1:8080/; }同样的请求后端实际收到的 URI 变成了/user?id1。也就是说location 里匹配到的/api/这段前缀被 proxy_pass 后面的/替换掉了。为什么会这样因为 Nginx 在“带 URI”的 proxy_pass 场景下会把请求 URI 中匹配 location 前缀的部分截掉再用 proxy_pass 中的 URI 拼接上去。你可以把 location 理解成一把剪刀匹配到的部分被剪掉proxy_pass 里的路径再贴上去。理解这个机制之后很多 404 问题就解释得通了你前端请求/api/userNginx 转发给后端时把/api剪掉了后端去查/user结果后端只注册了/api/user这个路由于是 404。生活里好比你寄快递一种方式是原包装转走一种方式是拆掉外包装只把里面的东西送过去。location 就是那层外包装。另外还有一个进阶坑当 proxy_pass 后面跟的是变量时比如location /api/ { set $backend http://127.0.0.1:8080; proxy_pass $backend; }只要 proxy_pass 的 URL 里出现了变量Nginx 就不会再像普通写法那样自动处理“剪掉前缀再拼接”的逻辑。它只是把变量解析后的值作为转发目标原始请求 URI 仍然会跟随过去。如果你在这种模式下还想去掉前缀就得自己拼变量比如set $backend http://127.0.0.1:8080/;再配合 rewrite 去改 URI。这个点记不住的话最安全的做法是除非你非常清楚自己在干什么否则 proxy_pass 后面尽量只写不变量、不带 URI 的地址。2. 高频场景配置拆解2.1 静态资源与接口分离一个很典型的配置大多数前后端分离项目的 Nginx 配置本质上就两类事静态文件直接让 Nginx 返回接口请求反代到后端服务。这个场景最适合用来理解 location 的边界划分。我给出一个完整的 server 块server { listen 80; server_name example.com; root /var/www/html; location / { try_files $uri $uri/ /index.html; } location ^~ /static/ { expires 7d; add_header Cache-Control public; } location /api/ { proxy_pass http://127.0.0.1:8080; 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; } }这个配置里/用来承接前端路由文件存在就直接返回不存在就回退到index.html交给前端路由处理。/static/用^~是为了防止后面新增正则 location 时误伤静态资源比如某个图片路径被正则匹配走。/api/全部反代给后端后端进程监听在 8080 端口。这里最关键的两个 header 是Host和X-Forwarded-For。后端如果需要根据域名做逻辑判断Host必须带上原始域名X-Forwarded-For则用来记录客户端真实 IP否则后端只能看到 Nginx 的 IP这在做用户日志、封禁、审计的时候会有麻烦。2.2 多个路径一并转发有几种写法有人问“Nginx location 多个路径一并转发”怎么做。场景通常是后端有多个服务前缀比如/api、/api2、/api3或者/user、/order、/product你想让它们全部转发到同一个后端。最简单的办法就是一个一个写 location每个都配一遍 proxy_pass但代码重复太多看着也烦。这时候可以借助正则 location 来合并location ~ ^/(api|api2|api3)(/.*)?$ { proxy_pass http://127.0.0.1:8080; }这条规则能匹配/api/user、/api2/order、/api3由于 proxy_pass 没带 URI原始 URI 会原样传给后端。正则里的捕获组只用来做匹配判断不参与转发拼接。注意正则 location 是区分大小写的如果服务前缀里有大写就需要改成~*或者再写一条。还有一种常见的多路径转发场景是“同一路径前缀转发到不同后端”比如/app1转发到 8081/app2转发到 8082。这种情况用多个普通 location 反而更清晰别强行用正则合并。配置的本质是让人一眼看懂不是为了炫技。如果你真的是路径很多、动态变化可以考虑用变量方式灵活转发location /api/ { set $backend http://127.0.0.1:8080; proxy_pass $backend; }这样做的优势是后续可以用 if 或 map 指令动态切换上游地址适合做灰度发布。但要注意变量方式少了一层 Nginx 的“自动补 URI”能力所以 proxy_pass 里不带 URI 时请求 URI 是原样保留的如果想让不同条件转发到不同后端务必要在变量里写完整地址然后统一不带 URI 转发。2.3 只转发带参数的 URL这个需求很实在“nignx 限制只转发带参数的 url”这个需求说白了就是某个接口请求如果没有携带 query string就直接拒绝不转发给后端。常见目的有几个防止空请求刷后端连接、要求请求必须带签名参数、或者给后端减少无效流量。实现方式非常直接location /api/ { if ($query_string ) { return 404; } proxy_pass http://127.0.0.1:8080; }这样/api/user会被 Nginx 直接返回 404而/api/user?nametest才会被转发到后端。如果你希望更严格一点只放行某些参数例如必须包含token可以这样location /api/ { if ($query_string !~ token) { return 403; } proxy_pass http://127.0.0.1:8080; }这里使用了一个正则否定匹配。$query_string是 Nginx 内置变量指原始请求中的 query 部分$args和它基本等价二者在绝大多数场景下可以互换。需要提醒的是Nginx 的if指令在很多场景下有众所周知的坑比如在 location 内使用if时如果里面写proxy_pass某些旧版本会报 “proxy_pass cannot have URI part in location given by regular expression” 之类的错误。所以我的经验是if 里只做return或者rewrite绝对不要在 if 里写 proxy_pass。上面的示例只用了 return是安全的。还有一个小细节如果用户请求的是/api/?此时$query_string也是空字符串也会被拦截。这通常不是问题但如果你的业务里空 query 和完全不带 query 有区别这个拦截逻辑需要再调整比如通过$request_uri判断原始请求是否以?结尾。2.4 反向代理与负载均衡高级一点的玩法当后端不止一台机器时就需要引入 upstream 来定义一组后端服务器。Nginx 默认的负载均衡策略是轮询也可以按权重分配。upstream backend_pool { server 127.0.0.1:8080 weight3; server 127.0.0.1:8081 weight1; server 127.0.0.1:8082 down; keepalive 32; } server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend_pool; proxy_http_version 1.1; proxy_set_header Connection ; 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; } }weight 字段表示权重down标记的服务器不会接收请求适合在发布维护时临时禁用某台机器。max_fails和fail_timeout可以控制健康检查的失败次数与冷却时间比如一台机器连续 3 次失败30 秒内不会再往它转发请求。为什么要在反向代理场景里加上proxy_http_version 1.1和空的Connection头因为后端如果开启了 keepaliveNginx 默认使用 HTTP/1.0 与该上游通信时无法复用连接。设置成 HTTP/1.1 并清掉 Connection 头才能让 Nginx 与后端保持长连接减少每次请求的 TCP 握手开销高并发时候体感差异非常明显。还有一点经常被忽略upstream 定义在后端是端口不是 URI。所以 proxy_pass 写http://backend_pool就够了千万不要写成http://backend_pool/。加了/意味着你要替换路径前缀等于是剪掉 location 匹到的部分然后拼上一个空路径。很多人在配置手册上随手抄成带斜杠的写法结果所有接口路径都变了排查了半天才发现多了一个斜杠。2.5 给 index.php 访问加上身份验证处理 401 场景“nginx 怎么配置 index.php 401 验证身份”其实是 Nginx 的auth_basic模块在起作用。这个模块基于 HTTP Basic Auth浏览器会弹出一个登录框输入用户名密码后请求头里会带上Authorization如果服务端校验失败就返回 401。先准备密码文件# 首次创建 htpasswd -c /etc/nginx/.htpasswd admin # 追加用户 htpasswd /etc/nginx/.htpasswd user1 # 如果没有 htpasswd 命令可用 openssl 生成 openssl passwd -apr1生成的文件格式是用户名:加密后的密码一行一个用户。配置文件权限的时候注意Nginx worker 进程需要能读到这个文件。因为通常 Nginx 的 worker 以 nginx 用户运行比如user nginx;所以文件别放在 root 用户才能读的目录里最稳妥的是设置 640 权限属主改为 nginx。然后在需要保护的 location 里加上配置location /admin/index.php { auth_basic Admin Area; auth_basic_user_file /etc/nginx/.htpasswd; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/tmp/php-fpm.sock; }这个配置表示访问/admin/index.php时必须先通过身份验证。如果要把整个/admin/目录都保护起来就把这些指令放在location /admin/ { ... }里。关于 401我最常遇到的问题是密码文件权限不对导致 Nginx 读不了但日志里显示的却是 “user not found” 或者 401。排查时可以先用命令确认文件可读sudo -u nginx test -r /etc/nginx/.htpasswd echo readable还可以用 htpasswd 校验密码是否匹配htpasswd -vb /etc/nginx/.htpasswd admin yourpassword如果校验通过但浏览器还是 401再看浏览器是否真的发送了 Authorization 头。有时候是前端页面在 iframe 里嵌了后台弹不出登录框也会一直 401。3. 完整实操从零配置一个可用反向代理3.1 一份能直接改的完整配置综合前面的知识点我写一份可以直接拿去改的完整配置涵盖静态文件、接口反代、多路径转发、参数限制、负载均衡和身份验证。upstream backend_java { server 127.0.0.1:8080 weight3 max_fails3 fail_timeout30s; server 127.0.0.1:8081 weight1 max_fails3 fail_timeout30s; keepalive 32; } server { listen 80; server_name example.com; root /var/www/html; index index.html index.htm; # 前端页面回退到 index.html location / { try_files $uri $uri/ /index.html; } # 静态资源直接返回不走后端 location ^~ /static/ { expires 7d; add_header Cache-Control public; } # 所有 /api/ 请求转发到后端集群且必须带参数 location /api/ { if ($query_string ) { return 404; } proxy_pass http://backend_java; proxy_http_version 1.1; proxy_set_header Connection ; 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; } # 额外路径一并转发 location /open/ { proxy_pass http://backend_java; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 后台 php 管理页面需要身份验证 location /admin/index.php { auth_basic Admin Area; auth_basic_user_file /etc/nginx/.htpasswd; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/tmp/php-fpm.sock; } }这份配置覆盖了前端静态资源、后端 API 反代、参数拦截、多路径转发、PHP 管理后台鉴权。你实际使用时把 server_name、root、upstream 的 IP 端口换成自己的就行。3.2 配置自检与重载别跳步改完配置第一步永远是校验语法nginx -t如果输出syntax is ok和test is successful再执行重载nginx -s reload用 systemd 管理 Nginx 的也可以systemctl reload nginxnginx -t只检查语法不检查逻辑。语法没问题不代表匹配结果符合预期所以重载之后一定要发请求验证。最直接的验证方式是用 curl# 验证前端路由 curl -I http://127.0.0.1/ # 验证 API 转发带参数 curl -I http://127.0.0.1/api/users?page1 # 验证不带参数时被拦截 curl -I http://127.0.0.1/api/users # 验证静态文件 curl -I http://127.0.0.1/static/app.js # 验证身份验证 curl -u admin:password -I http://127.0.0.1/admin/index.php生产环境如果有多域名curl 访问本机时需要带上 Host 头curl -H Host: example.com -I http://127.0.0.1/api/users?page1否则 Nginx 按默认 server 处理可能跑到另一套配置上。3.3 请求到达了哪条 location用一个临时头验证如果 location 写得多有时候没法判断真正命中哪一条。我的调试习惯是在 location 里临时加一个响应头带上当前匹配到的 URI肉眼一看就知道了。location /api/ { add_header X-Debug-Location $uri; proxy_pass http://backend_java; }然后 curl 看响应头curl -I http://127.0.0.1/api/users?page1响应头里如果出现X-Debug-Location: /api/users说明这个请求确实被这个 location 接管了。验证完记得删掉调试头避免泄漏内部信息。更常用的验证方式是在 access log 里看结果配置的 log_format 中最好包含$request_uri、$uri、$upstream_addr这些变量。$request_uri是客户端原始请求的完整 URI$uri是 Nginx 解码后的当前 URI$upstream_addr是实际转发到的上游地址。三者一对比就能看出 URL 在哪一步被改写了。log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $request_uri $uri $upstream_addr;把这段 log_format 配置到 http 块里并让 access_log 指向它然后重载 Nginx就可以在日志里精确看到每一个请求的转发轨迹。3.4 排查时一定要对比“直连后端”和“经过 Nginx”很多 404、502 问题我需要先判断是 Nginx 的问题还是后端的问题。方法很简单先用 curl 直接访问后端确认后端本身是通的。curl -i http://127.0.0.1:8080/api/users?page1如果直连后端能返回正常结果但访问 Nginx 代理后的地址就是 404那问题基本锁定在 proxy_pass 的 URI 改写逻辑或 Header 传递上。这时候把两条请求的 URL 放一起比对重点看路径里/api/前缀还在不在。比如直连是/api/users经过 Nginx 后变成/users那就要去检查 proxy_pass 后面是不是多写了/。如果直连后端也超时或 502那问题就在后端服务本身可能是端口没监听、进程挂了、数据库连接满了。这时候 Nginx 怎么调都没用先把后端救起来。4. 常见问题与排查技巧实录我整理了平时遇到频率最高的一批问题做成表格方便直接按图索骥。现象常见原因排查方法解决办法502 Bad Gateway上游服务没启动、端口不对、防火墙拦截直连后端 curlnetstat 查端口启动后端服务修正 proxy_pass 地址504 Gateway Timeout后端处理超时看后端日志调大proxy_read_timeout、检查后端慢查询404 Not Foundproxy_pass 带 URI 导致路径被替换后端没有对应路由对比直连后端和经 Nginx 的 URI去掉 proxy_pass 里多余路径改用不带 URI 写法401 Unauthorized密码文件权限不对、用户密码不匹配、浏览器未弹窗htpasswd -v 校验检查文件可读性重新生成密码文件并设置权限请求匹配到了错误 location正则命中优先级高或^~使用不当临时添加调试头 X-Debug-Location调整 location 顺序或加^~前缀无限重定向Host 头被替换后端返回了指向其他域名的跳转curl -I 看 Location 响应头设置proxy_set_header Host $host;和X-Forwarded-Proto静态文件被代理到后端location 正则优先级高于普通前缀确认匹配结果使用^~ /static/强制终止正则匹配带参数接口全部被拦$query_string的判断条件写错查看$query_string值用日志输出$args和$query_string对比再补充几条经验一是proxy_pass后面带 URI 的写法尽量少用。大多数情况下你要的是透明转发不需要 Nginx 帮你改路径。那些“去掉前缀再转发”的需求其实应该在后端路由层面解决而不是靠 Nginx 剪路径。因为后端如果换了前缀Nginx 配置也要跟着动维护成本很高。二是 location 匹配规则不要死记直接用一个临时测试 location 来验证。例如location /test/a/ { add_header X-Debug test-a; return 200; }配完 curl 一下对应路径看返回的响应头多试几次就记住了。三是如果你用了auth_basic又发现 Nginx 返回 403 而不是 401那大概率是权限校验过了但要访问的文件本身就不可读比如 PHP-FPM 执行的文件不在 Nginx 允许范围内。401 和 403 的区别在于401 是身份认证失败403 是服务器理解了请求但拒绝执行。看到 403 时别纠结密码文件去查目录权限和后端服务。四是关于“Nginx 限制只转发带参数的 url”的限制它确实能挡住一部分空请求但并不能作为安全手段因为 query string 是可以随意构造的。真正要做接口鉴权应该在后端校验签名或 Token。Nginx 这一层只是起到过滤和降负载的作用不要把安全全部寄托在它身上。最后说一点个人体会。location 和 proxy_pass 这类基础指令很多人觉得看懂了就等于会了但实际踩坑大多不是语法问题而是对“匹配顺序”和“URI 替换规则”的理解偏差。我给自己定了条规矩每次改完配置先nginx -t再用 curl 带真实路径验证一遍而且至少要验两条一条带参数一条不带参数这样大部分问题在发版前就能暴露。如果你是因为 502 或 404 熬夜排查才搜到这篇文章希望这篇能帮你省下几个小时的折腾时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门