Nginx 443 端口 SNI 分流:WebSocket 与普通请求共用配置

    发布时间: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 服务块,这样证书与后端路由就能各归各位。

    为什么 http 层的 map 解决不了证书问题

    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 层 ssl_preread 做 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 层处理。

    http 层两个 server 块如何接住流量

    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