发布时间:2026-09-24 12:30 更新时间:2026-09-24 12:30 阅读量:0
很多站长的服务器上只开放了 443 一个端口,却要同时承载普通 HTTPS 页面和 WebSocket 长连接。最常见做法是让 Nginx 在 http 层用 map 判断 Upgrade 头再 proxy_pass 到不同后端,但一旦前后端域名不同、证书各挂各的,就会出现「证书和请求对不上」的报错,浏览器直接提示连接不安全。根因不在 http 层,而在 TLS 握手阶段:证书是按 SNI 挑的,如果握手时就已经走错了 server 块,后面再 map 也救不回来。
本文的思路是把 TLS 结束点往下挪一层,用 stream 模块的 ssl_preread 在握手前读出域名,按域名把流量丢给不同的 http 服务块,这样证书与后端路由就能各归各位。
Nginx 处理 HTTPS 时,第一步是 TLS 握手,握手时要根据客户端发来的 SNI(Server Name Indication)选择对应的 server_name 和证书。这个动作发生在 HTTP 请求解析之前,也就是说 map $http_upgrade 这类判断要等握手完成、请求头读到之后才生效。
问题就出在这里:如果普通站点和 WebSocket 站点用的是两个域名、两张证书,而它们都挤在同一个监听 443 的 server 块里,那么无论后面怎么分流,握手阶段只能挑一张证书。客户端访问 A 域名,却拿到 B 域名的证书,浏览器自然报证书不匹配。
想验证当前证书情况,可以用 openssl 直接看某个域名握手时拿到的证书:
openssl s_client -connect api.example.com:443 -servername api.example.com /dev/null | openssl x509 -noout -subject -issuer -dates
把 -servername 换成另一个域名再跑一次,如果两次输出的 subject 一样,说明证书没有按 SNI 区分,这就是要动手改的地方。实际输出以你的证书为准。
stream 模块工作在 TCP 层,不解析 HTTP,但它的 ssl_preread 可以在不结束 TLS 的情况下,从 ClientHello 里把 SNI 字段读出来,存到变量 $ssl_preread_server_name。有了这个变量,就能按域名把 TCP 连接转发到不同的本地端口。
整体架构是这样:stream 层监听 443,按 SNI 把流量分给本地 127.0.0.1:8443(普通站点)和 127.0.0.1:8444(WebSocket 站点);http 层再分别监听这两个端口,各自配好自己的证书和 location。这样每个域名在握手时都能拿到正确的证书。
先确认编译参数里有 stream 和 ssl_preread 支持:
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'stream|preread'
如果输出里能看到 --with-stream 和 --with-stream_ssl_preread_module,就可以直接用;没有的话需要重新编译或换用带这些模块的发行版,具体以官方文档为准。
stream 层配置示例,写在 /etc/nginx/nginx.conf 的顶层(与 http 同级):
stream {
map $ssl_preread_server_name $backend_port {
ws.example.com 8444;
api.example.com 8444;
default 8443;
}
upstream plain_https {
server 127.0.0.1:8443;
}
upstream websocket_https {
server 127.0.0.1:8444;
}
server {
listen 443;
ssl_preread on;
proxy_pass $backend_port;
proxy_protocol on;
}
}
这里用 proxy_pass $backend_port 直接按变量转发,变量值就是端口号,Nginx 会转发到本机对应端口。也可以用两个 upstream 配合 if 判断,但按变量转发更直观。注意 stream 里的 server 不能写 ssl_certificate,因为这一层不做 TLS 终止,证书统一交给后面的 http 层处理。
stream 分出来的两个端口,在 http 层要各自有对应的 server 监听。普通站点监听 8443,WebSocket 站点监听 8444,两个块各挂自己的证书,互不干扰。
普通站点的配置大致如下:
server {
listen 127.0.0.1:8443 ssl proxy_protocol;
server_name www.example.com;
ssl_certificate /etc/nginx/cert/www.example.com.crt;
ssl_certificate_key /etc/nginx/cert/www.example.com.key;
location / {
root /var/www/site;
index index.html;
}
}
WebSocket 站点的配置,重点是 Upgrade 头透传和长连接超时:
server {
listen 127.0.0.1:8444 ssl proxy_protocol;
server_name ws.example.com;
ssl_certificate /etc/nginx/cert/ws.example.com.crt;
ssl_certificate_key /etc/nginx/cert/ws.example.com.key;
location /ws/ {
proxy_pass http://127.0.0.1:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
两个要点:一是 proxy_protocol 必须和 stream 层的 proxy_protocol on 配套,否则后端拿到的真实 IP 会变成 127.0.0.1,日志和访问控制都会失真;二是 WebSocket 的 proxy_read_timeout 要按业务心跳间隔设置,设得太小会出现连接空闲被掐断的情况。改完执行 nginx -t 检查语法,再 systemctl reload nginx 平滑生效。
第一类坑是 proxy_protocol 只开了一边。stream 层开了,http 层 listen 上没加,Nginx 启动会报错或者解析出乱码 IP;反之 http 层加了但 stream 层没开,握手直接失败。两边必须成对出现。
第二类坑是 map 的 default 分支。如果某个域名没在 map 里登记,会落到默认端口,结果是「证书对上了但页面 404」或者反过来。建议把未知域名统一指向一个只返回 444 的默认 server,避免暴露多余信息。
第三类坑是 IPv6。如果服务器同时监听 IPv6,stream 层 listen 443; 要确认是否写成了 listen [::]:443;,否则 IPv6 客户端会连不上,而 IPv4 测试一切正常,排查时容易忽略。
验证分三步:先用前面那条 openssl s_client -servername 命令确认每个域名拿到的是自己的证书;再用 curl -v https://ws.example.com/ws/ 看响应头是否正常;最后在浏览器开发者工具的 Network 面板里看 WebSocket 连接状态码是不是 101。三步都通过,说明 SNI 分流链路是通的。
如果后端还要区分真实 IP,记得在 http 层配合 set_real_ip_from 127.0.0.1; 和 real_ip_header proxy_protocol;,把 stream 层透传过来的地址还原出来,否则日志里全是本机地址,后续排查会更费劲。
| 📑 | 📅 |
|---|---|
| Docker 容器内 Nginx 与宿主 Nginx 端口冲突排查 | 2026-09-24 |
| Nginx 与 CDN 回源 IP 不一致:XFF 取值顺序与日志字段 | 2026-09-24 |
| Linux负载高但CPU空闲:D状态进程与不可中断睡眠排查 | 2026-09-24 |
| Redis 主从切换后写入报 READONLY:三种拓扑的误配排查 | 2026-09-23 |
| PostgreSQL表膨胀与autovacuum不生效排查实战 | 2026-09-23 |
| MySQL 8 大事务致 undo 膨胀与主从延迟:定位与拆分提交 | 2026-09-24 |
| Docker 容器内 systemctl 不可用:init 缺失与多进程管理的取舍 | 2026-09-24 |
| Nginx 反代后协议错乱:X-Forwarded-Proto 排查与修复 | 2026-09-24 |
| Linux 服务器 OOM Killer 日志怎么看:dmesg 与 journalctl 定位被杀进程 | 2026-09-25 |
| 内核日志里的 OOM 线索:oom_score、cgroup 限制与判断内存不足原因 | 2026-09-25 |