关键词:nginx / HTTPS / 反向代理 / 跨域 / rewrite / 永久跳转 / 泛二级域名 / resolver / server_names_hash_bucket_size
引言:当域名、证书和转发规则「搅在一起」
有个同事接了个看似简单的需求:
- 把
http://www.test.com转到内部服务127.0.0.1:6666; - 顺带上个 HTTPS,给前端 API 用;
- 再加一下 CORS,方便本地调试。
结果一顿操作之后:
- HTTP 可以访问,但被浏览器标红「不安全」;
- HTTPS 能连上,但接口 502,日志里全是「no resolver defined」之类的提示;
- 跨域头随缘返回,有时候 OPTIONS 直接 403。
域名、证书、转发、跨域,这些看起来零散的需求,最后都会聚在 nginx 这一层。
这一篇,我们沿着一个「入口流量」的视角,把 HTTPS、反向代理、CORS、永久跳转、泛二级域名这些高频场景串成一条线:
让你对「nginx 作为流量入口」有一个完整、可落地的实战套路。
一、HTTPS:让 TLS 仅仅是「多写几行配置」
HTTPS 本身的协议细节可以很复杂,但在 nginx 里,最基础的启用方式其实就几行:
nginx
server {
listen 443 ssl;
server_name example.com;
ssl_certificate cert.crt;
ssl_certificate_key cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html index.htm;
}
}
几个要点:
listen 443 ssl:没有ssl关键字,nginx 不会以 TLS 模式监听;ssl_certificate/ssl_certificate_key:公钥证书 + 私钥;- 其余如
session_cache、ciphers是典型的「按安全基线抄」配置。
工程实践里,HTTPS 真正的难点往往不是「写哪几行」,
而是证书申请、更新自动化,以及如何把 HTTP 流量低成本地引到 HTTPS 上。
二、反向代理:把前端流量稳稳送到后端
最常见的 nginx 用法,依旧是反向代理:
把「对外的域名」映射到「内部的服务」上。
1. 标准反向代理模板
例如,把 http://www.test.com 转发到本地 127.0.0.1:6666:
nginx
server {
listen 80;
server_name www.test.com;
location / {
# 透传真实客户端 IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
# 代理连接超时设置为 10 秒,默认 60 秒太长
proxy_connect_timeout 10;
proxy_pass http://127.0.0.1:6666;
}
}
几个实践建议:
- 不要用
localhost,对于某些配置(尤其结合变量)会踩到解析坑,IP 更稳妥; - 一定要设置
proxy_connect_timeout,否则后端服务半死不活时,前端会等到怀疑人生; - 把
Host透传给后端,很多应用会根据 Host 做路由或多租户判断。
2. 再加一层:入口 + BFF + 后端
在真实生产环境中,nginx 一般只是「最外面那层」:
nginx把流量打到 BFF(Node.js / Nest / Hono);- BFF 再去调内部服务(Go / Java / 微服务集群)。
nginx 里这层的目标只有两个字:干净。
- 不做业务逻辑,只做协议层的事情:TLS、限流、基本鉴权、CORS、流量分流;
- 其余复杂逻辑交给 BFF / 网关层处理。
三、CORS:把跨域放在入口统一处理
前端最常见的一句抱怨:
「明明后端已经在接口里加了 CORS,怎么浏览器还是说跨域?」
如果你的 nginx 在最外层挡着,而后端只给应用服务加了 CORS,很可能出现:
- 预检(OPTIONS)被 nginx 拦下了,根本没到后端;
- 或者 nginx 自己返回了 4xx/5xx,却没带任何 CORS 头。
最简单的解决方式,就是直接在 nginx 入口层加 CORS 头:
nginx
server {
listen 80;
server_name www.test.com;
location / {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Headers' 'Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With' always;
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS' always;
# 如果需要处理 OPTIONS 预检,可以单独拦一条
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:6666;
}
}
要点:
always可以确保即便返回 4xx/5xx 也带上 CORS 头;- 尽可能在入口层统一处理 CORS,而不是每个后端服务都写一遍。
CORS 这种「协议层」的东西,
放在入口统一做,比撒在各个业务代码里要可靠得多。
四、永久跳转:把「历史域名」优雅收束起来
历史域名、带不带 www、从 HTTP 到 HTTPS,这些需求本质上都是「跳转」问题。
1. 从带 www 跳到不带:www → 非 www
例如,将 http://www.wangmiaozero.cn 永久跳转到 http://wangmiaozero.cn,并丢弃路径:
nginx
server {
listen 80;
server_name www.wangmiaozero.cn;
location / {
rewrite ^(.*) http://wangmiaozero.cn permanent;
}
}
这里的 permanent = 301,浏览器会长期记住。
2. 从 HTTP + www 跳到 HTTPS + 非 www
如果希望:
http://www.wangmiaozero.cn/foo
→ 永久跳转到
https://wangmiaozero.cn/foo(保留路径):
nginx
server {
listen 80;
server_name www.wangmiaozero.cn;
location / {
rewrite ^(.*) https://wangmiaozero.cn$document_uri permanent;
}
}
注意:
$document_uri是「不含查询参数」的路径;- 如果需要连 query 一起保留,可以用
$request_uri。
推荐做法是:
所有 HTTP 流量统一跳到 HTTPS + 归一后的主域名 ,用户访问路径尽量保持不变。
五、泛二级域名:一套配置搞定「N 个子站点」
当你有一堆类似:
tenant-a.example.comtenant-b.example.comfoo.internal.example.com
的需求时,就轮到「泛二级域名」登场了。
1. 基于转发的泛二级域名:不同子域名 → 不同后端路径
目标:
http://*.wangmiaozero.cn/ 统一转发到 http://127.0.0.1:8080/*/。
示例配置:
nginx
http {
include mime.types;
default_type application/octet-stream;
# 重要:当 proxy_pass 里用变量构造地址时,必须配置 resolver
resolver 8.8.8.8;
sendfile on;
keepalive_timeout 65;
# 某些特殊域名单独写在前面
server {
listen 80;
server_name www.wangmiaozero.cn www.test.com test.com;
location / {
rewrite ^(.*) http://wangmiaozero.cn permanent;
}
}
server {
listen 80;
server_name demo.wangmiaozero.cn;
location / {
root D:/Workspace/github/demo;
index index.html index.jsp;
}
}
# 泛二级域名:匹配 *.wangmiaozero.cn
server {
listen 80;
server_name ~^(.+)?\.wangmiaozero\.cn$;
location / {
# 注意这里要用 127.0.0.1,不要用 localhost
proxy_pass http://127.0.0.1:8080/$1$request_uri;
}
}
}
说明:
server_name ~^(.+)?\.wangmiaozero\.cn$用正则把子域名拿出来;$1就是子域名,可以直接拼在转发路径里;- 前面可以加若干「特殊子域名」的
server,nginx 会先命中那些更具体的配置。
2. 基于目录的泛二级域名:不同子域名 → 不同静态目录
如果你想把 a.wangmiaozero.cn 映射到 D:/Workspace/github/test/a,
b.wangmiaozero.cn 映射到 .../test/b,可以这样写:
nginx
server {
listen 80;
server_name ~^(.+)?\.wangmiaozero\.cn;
location / {
root D:/Workspace/github/test/$1;
index index.html index.jsp;
}
}
这套配置对「多租户静态站点」特别友好:
- 每个子域名一个目录;
- 上线只需要把对应目录挂上去。
3. no resolver defined to resolve localhost:别再踩一次的坑
当你在 proxy_pass 里用变量拼接地址时,例如:
nginx
proxy_pass http://$http_host$request_uri;
如果没有配置 resolver,nginx 会报:
text
no resolver defined to resolve localhost
这就是前面配置里那句:
nginx
resolver 8.8.8.8;
存在的意义:当 nginx 需要在运行时解析一个域名(通过变量构造时),必须有 DNS 可以用。
六、server_names_hash_bucket_size:域名多了,表要加大
当你在一个 http {} 里写了大量 server_name 时,
有可能在 reload 时看到这样的错误:
text
could not build the server_names_hash, you should increase server_names_hash_bucket_size: 64
大意就是:
「存域名的 hash 桶不够大了,请你调大点」。
解决办法也很简单,在 http {} 里加一行:
nginx
http {
include mime.types;
default_type application/octet-stream;
# 域名过多时需要配置这个参数,一般是 2 的指数,比如 512
server_names_hash_bucket_size 512;
}
工程视角下,这类「hash bucket」报错的本质,
就是 nginx 在帮你暗戳戳管理一张「域名路由表」,
当这张表太挤时,你需要多给它一点空间。
七、路径追加规则:root / proxy_pass 到底怎么拼 URL?
路径拼接规则,是另一个高频「凭感觉写」然后翻车的地方。
看一段典型配置:
nginx
server {
listen 80;
server_name www.test.com;
location / {
root E:/Workspace/test/htdocs/;
index index.html;
}
location /test_root {
# 访问 www.test.com/test_root/aaa/index.html
# 实际访问的是 htdocs/test_root/aaa/index.html
root E:/Workspace/test/htdocs/;
index index.html;
}
location /test_proxy1 {
# 访问 www.test.com/test_proxy1/aaa/index.html
# 实际访问 www.proxy.com/test_proxy1/aaa/index.html
proxy_pass http://www.proxy.com;
proxy_buffering off;
}
location /test_proxy2 {
# 访问 www.test.com/test_proxy2/aaa/index.html
# 实际访问 www.proxy.com//aaa/index.html
proxy_pass http://www.proxy.com/;
proxy_buffering off;
}
location /test_proxy3 {
# 访问 www.test.com/test_proxy3/aaa/index.html
# 实际访问 www.proxy.com/bbb/aaa/index.html
proxy_pass http://www.proxy.com/bbb;
proxy_buffering off;
}
}
可以总结出几个实用结论:
-
root+location:location /foo+root /var/www→ 实际路径是/var/www/foo/...;- 不管
foo后面写不写/,location里的那段都会被拼到真实路径上。
-
proxy_pass:- 以不带
/结尾的形式(如http://www.proxy.com):会保留原始路径前缀; - 以
/或子路径结尾(如http://www.proxy.com/、http://www.proxy.com/bbb):会替换掉location的那一段前缀。
- 以不带
简单记忆:
root是「前缀 + 原路径」,proxy_pass则取决于你最后写没写/。
八、nginx 做代理服务器:能用,但别太依赖
理论上,你甚至可以用 nginx 搭一个通用 HTTP 代理:
nginx
# 代理服务器
server {
listen 6587;
resolver 8.8.8.8;
location / {
proxy_pass http://$http_host$request_uri;
#allow 127.0.0.1;
#deny all;
}
}
然后把浏览器的代理设置成 192.168.1.111:6587(假设 nginx 在这台机器)。
这玩意儿能跑,但问题也很多:
- 不支持 HTTPS 透明转发;
- 对某些协议 / 头部支持不完善;
- 稳定性和观测能力都比专业代理差太多。
大部分场景下,更推荐用:
- 专门的 HTTP/HTTPS 代理(mitmproxy、Squid、Charles 等);
- 或者在网关 / sidecar 层做统一代理逻辑。
把 nginx 当万能代理工具,只是「能用」,
把它当成清晰的流量入口,才是「好用」的正确打开方式。
九、总结:让 nginx 真正成为「流量入口的工程设施」
- HTTPS 层面,nginx 的目标是:让证书接入和 HTTP→HTTPS 跳转变成标准套路;
- 反向代理层面,它是最外面那层「干净的流量路由」,只做协议相关的事情;
- 跨域层面,在入口统一加 CORS,比在每个后端服务里散着写要可靠;
- 域名与泛域名层面,通过正则
server_name+resolver+hash bucket,可以优雅地管理一堆历史域名与子域名; - 路径与转发层面,搞清楚
root与proxy_pass的拼接规则,就能少掉一大半「怎么转发到奇怪路径」的 Bug。
上篇我们解决了「请求到底会命中哪条 location」的问题,
这一篇把目光拉到流量入口,讲清了域名、证书与转发背后的工程套路。
下一篇(下篇),我们会从「nginx 的全局变量与调试思路」出发,
讨论如何在复杂系统里,把 nginx 从「黑盒」变成真正可观测、可诊断的一部分。