恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
WebSocket如何配置wss访问?nginx反向代理、证书与心跳全解析
首页
资讯中心
/
WebSocket如何配置wss访问?nginx反向代理、证书与心跳全解析
WebSocket如何配置wss访问?nginx反向代理、证书与心跳全解析
发布时间:2026/10/6 3:47:16
简介这是一份面向Java后端开发者的Spring Boot WebSocket安全通信示例资源适合已掌握Spring Boot基本用法、希望在生产环境启用wss安全连接的工程师学习参考。资源基于Spring Boot 2.1涵盖SSL/TLS加密原理、依赖引入、证书导入、内嵌Tomcat端口配置、自定义WebSocket处理器以及前端wss连接等关键模块能帮助读者系统理解HTTPS下长连接的完整实现。包内共66个文件包含6个Java源文件、6个XML配置文件、10个sample示例另有可直接使用的jks证书、Maven构建脚本和.git版本记录压缩后仅61KB结构简约便于导入工程或复用代码。已有13110人学习使用。有了这套示例开发者可以快速完成wss环境的搭建与验证避免证书配置和协议切换带来的常见坑点特别适合在线聊天、实时推送、股票行情等需要安全双向通信的场景。1. 给WebSocket套上TLSwss配置这四步到底在做什么你在HTTPS页面里写new WebSocket(ws://api.example.com/ws)控制台大概率直接甩你一条 Mixed Content 报错连接请求还没发出就被浏览器拦停。webSocket配置wss访问就是解决这类问题的把连接串从ws://换成wss://让WebSocket跑在TLS加密通道里同时把端口从 80 转到 443。它解决的不只是加密——HTTPS页面只能发起wss证书不被信任的wss也会被拒nginx反向代理、负载均衡、心跳超时全都跟着联动。这篇笔记适合后端、运维和写实时页面的前端按下面给的配置和排查路径从证书到联调一次跑通。2. 从ws到wss连接链路、协议差异与三套落地路线怎么选2.1 一次wss连接从发起到101的完整链路wss和ws的差异不是“加个密码”而是整条链路换成TLS。你从浏览器输入wss://ws.example.com/ws实际发生的事是这个顺序浏览器先对ws.example.com:443发起TLS握手这一步和打开HTTPS网站完全一样证书校验、加密套件协商都在这里完成TLS隧道建好后浏览器在隧道里发HTTP GET请求带着Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key、Sec-WebSocket-Version: 13服务端确认协议版本和密钥字段合法返回101 Switching Protocols之后连接升级为WebSocket帧协议双向收发数据业务数据本身也在这条TLS隧道里所以是加密的。这段有个容易被忽略的点wss握手失败绝大多数发生在第1步TLS阶段而不是第3步WebSocket升级阶段。证书不受信任、TLS版本过低、证书链不完整浏览器在TLS握手阶段就掐断根本不会给你101的机会。所以排查wss问题时先看TLS层是我动手的第一步。这个顺序也解释了为什么WebSocket服务器本身没起TLS也一样能对外提供wss——TLS由nginx终结nginx和后端之间走明文ws这就是最常见架构。协议差异具体到参数上也很直观ws默认走80端口wss默认走443端口ws的握手请求是明文wss的握手请求在TLS隧道内ws可以被HTTP中间人设备观察甚至篡改wss的数据帧和握手都不可读。还有就是TLS版本nginx上如果只开TLSv1.2和TLSv1.3老设备用TLS 1.0就会在第一步失败这一点在排查“别人能连我不能连”时特别常见。2.2 三套主流wss落地路线nginx反代、应用层TLS、云网关托管wss落地不是只有一种做法选型要看部署形态。我接触过的方案可以分成三档。nginx反向代理是最常见的选择证书放在nginx上nginx把TLS解密后以明文ws转发给内网后端。好处是后端不用改一行代码Java、Go、Python、Node全都能接证书续期、负载均衡、限流全在nginx层解决。代价是多一层代理、多一个故障点但nginx对长连接的稳定性是经过大量生产验证的。应用层TLS是后端直接起HTTPS服务并在同一个端口挂WebSocketNode.js的https模块加ws库就是这么干的。省掉nginx链路短、延迟低但证书管理、多实例负载均衡、TLS终结的CPU开销都得自己扛适合服务本身是独立小项目、没有专职运维的场景。云网关托管是第三条路云负载均衡或API网关直接支持WebSocket协议转发证书挂在云控制台。好处是免运维、自带DDoS防护坏处是会话保持、超时参数、路径重写受平台文档约束调试起来像个黑匣子出了问题只能提工单。选型判断其实就一句后端不想动就上nginx链路想最短就应用层TLS不想管证书就上云网关。选型证书位置后端改动适合场景主要成本nginx反代nginx无多后端服务统一出口多一跳需维护nginx配置应用层TLS应用进程改启动配置单服务独立部署TLS握手耗CPU扩展需自己做云网关云控制台基本无没有专职运维参数受平台限制如果还没想清楚直接上nginx反代几乎不会出错。它就是给wss做代理的最成熟方案踩坑资料最多出问题也最容易找到参照。2.3 端口、证书、超时三个最容易拍脑袋定错的参数wss默认走443但很多人为了“不占用网站端口”故意开8443之类的高端口结果证书配好了连接还是失败。端口和证书是两回事证书校验只认域名不认端口但浏览器不会因为你用了8443就放松安全校验反而让用户记忆和防火墙放行都变复杂。我的习惯是能复用443就复用443nginx同一监听端口下用location区分WebSocket路径和普通HTTP路径代价最小。证书方面要分清两个场景。公网环境用Lets Encrypt这类公开CA签发的证书配合certbot自动续期90天有效期加一条定时任务就行别手动续。内网环境用自签证书或私有CA把CA根证书装进测试机的受信任根存储区能省掉开发调试阶段大半的脾气。自签证书适合临时验证不适合长期固定场景。超时参数是隐藏陷阱它不在wss协议里却在nginx和负载均衡配置里。WebSocket是长连接帧流nginx如果判断一条连接长时间没有数据流默认的proxy_read_timeout会掐断它这条规则对ws和wss一视同仁。所以配置wss时代理超时必须显式调大且要和客户端心跳间隔联动。并发量方面一台2核4G的机器做wss反代把系统打开文件数调到65535撑几千个长连接问题不大真正吃资源的是TLS握手和连接数本身这个量级够绝大多数业务用。3. 用nginx配置wss访问证书生成、反代配置与前后端联动改法3.1 先准备证书自签证书命令、证书链合并与私有CA入场wss配置的第一步是确保证书文件到位。内网联调最省事的做法是生成一张自签证书一条openssl命令搞定openssl req -x509 -newkey rsa:2048 -nodes \ -keyout /etc/nginx/cert/wss.key \ -out /etc/nginx/cert/wss.crt \ -days 365 \ -subj /CNws.example.com-nodes表示私钥不加密nginx启动时不用输入密码方便开机拉起-days 365给自签证书一年有效期别开到三年五年到期忘记换就是事故。这里生成的是自签x509证书浏览器会警告不受信任属于联调阶段专用。如果手头有公司私有CA签发的证书拿到的通常是服务器证书加一个中间证书nginx要求证书文件包含完整链路需要先拼起来cat wss.crt intermediate.pem /etc/nginx/cert/fullchain.crt顺序必须是服务器证书在前、中间证书在后拼反了某些客户端会报unable to get local issuer certificate。公网环境直接跑certbot certonly --standalone -d ws.example.com签出来的fullchain.pem和privkey.pem就是nginx要的两个文件。这段做完用下面命令自检格式openssl x509 -in /etc/nginx/cert/wss.crt -noout -subject -dates能看到subject里是目标域名、dates里是有效期证书这一步就齐了。证书是整个wss链路里最容易翻车的环节先把证书文件跑通再动nginx不然一出错你会分不清是nginx配置问题还是证书问题。3.2 nginx反代的核心配置每一条指令为什么这样写证书有了接下来挂到nginx上。我给的这份配置是wss反代的最小完整形态map块放在 http 块内server部分放站点配置map $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 443 ssl; server_name ws.example.com; ssl_certificate /etc/nginx/cert/fullchain.crt; ssl_certificate_key /etc/nginx/cert/wss.key; ssl_protocols TLSv1.2 TLSv1.3; location /ws { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }逐条说清楚每一行的原因。listen 443 ssl把TLS终结在这里wss默认就走这个端口ssl_certificate填含完整链的证书文件自签场景填wss.crt就行。map块把Upgrade头映射成Connection头的取值客户端发来Upgrade就用upgrade普通HTTP请求没有Upgrade头时Connection就是close这对同一个server块里既提供HTTPS静态资源又提供WebSocket服务特别重要避免普通请求也被强制保持长连接。proxy_http_version 1.1是wss反代的核心条件Upgrade机制是HTTP/1.1的特性nginx默认向后端发的是HTTP/1.0不改成1.1后端根本不认升级请求。proxy_set_header Upgrade $http_upgrade和proxy_set_header Connection $connection_upgrade这两行把客户端升级意图原样传给后端少了任何一行握手都会停在101之前。proxy_read_timeout和proxy_send_timeout调到3600秒防止nginx在长连接空闲时主动清理连接具体值和心跳的联动关系放到最后一章讲。3.3 路径转发与URL重写/ws和/的对应关系理清楚配置里location /ws的路径后缀和proxy_pass的尾斜杠是wss配置里第二个高频翻车点。前端连接wss://ws.example.com/ws后端WebSocket端点如果恰好监听在/上就需要把location配成转发到根路径location /ws/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass末尾有没有/语义完全不同。带/时nginx会拿掉location匹配到的前缀/ws把剩下部分拼到后端地址上不带/时整个/ws/xxx原样传给后端。前一节的proxy_pass http://127.0.0.1:8080;不带斜杠后端监听在/ws前端也连/ws两边一致就不会出问题。如果后端WebSocket端点是/echo前端想统一用/ws入口就在location里加rewritelocation /ws { rewrite ^/ws(/.*)$ /echo$1 break; proxy_pass http://127.0.0.1:8080; }rewrite ^/ws(/.*)$ /echo$1 break把开头的/ws替换成/echobreak表示rewrite后不再匹配新的location直接进入代理。这里还有个容易踩的坑如果同一个server块同时开了http2部分浏览器会把连接协商到HTTP/2它的WebSocket升级语义和HTTP/1.1不完全一样nginx上偶发握手异常。我的习惯是wss专用server块不开http2静态资源和WebSocket同域混用时按路径区分不指望同一个连接既跑H2又跑WebSocket升级。3.4 前端连接串和后端监听的联动js与Node.js的落地写法nginx配置完成只是半程前端连接串写死同样会让wss访问失败。最常见的问题是页面走HTTPS、WebSocket串却写成ws://这在HTTP时代没问题现在被浏览器混合内容策略直接拦截。前端正确写法是根据页面协议动态拼const WS_URL (location.protocol https: ? wss:// : ws://) location.host /ws; const ws new WebSocket(WS_URL);location.host自带域名和端口和nginx的server_name与listen 443一一对应前端不需要硬编码域名部署到哪都能自适应。如果非要用显式地址就写wss://ws.example.com/ws协议前缀、域名、路径三段必须和nginx的server_name与location完全对齐差一个字符都会握手失败。后端如果是Node.js配合nginx最稳的写法是起一个纯HTTP服务给WebSocket用TLS交给nginxconst http require(http); const WebSocket require(ws); const server http.createServer(); const wss new WebSocket.Server({ server }); wss.on(connection, (socket) { socket.on(message, (data) { socket.send(echo: ${data}); }); }); server.listen(8080);WebSocket.Server({ server })直接把WebSocket挂到HTTP服务上没有https.createServer因为TLS在nginx那层已经终结后端只监听8080明文ws。如果不想用nginx就把http换成https并传入cert和key监听443效果等同但证书管理和多实例扩展要自己负责。我一般建议先把nginx这条路跑通尤其是有多个后端服务需要统一挂wss的时候。4. wss配置避坑握手失败、混合内容与秒断的5个排查场景4.1 浏览器直接拒绝自签证书wss连不上现象浏览器控制台报net::ERR_CERT_AUTHORITY_INVALIDwss连接根本没进入WebSocket握手阶段。原因自签证书不在操作系统受信任根证书列表里TLS握手阶段被浏览器判为无效直接掐断。这不是配置写错是信任链缺失。解决内网联调时把自签证书的CA文件导入本机受信任根证书Windows在证书管理器导入macOS在钥匙串里设为始终信任Linux放到/usr/local/share/ca-certificates后执行update-ca-certificates导入后重启浏览器再连。公网环境老老实实用Lets Encrypt自签证书不该出现在对外服务上。确认修复是否生效很简单浏览器地址栏直接访问https://域名没有证书告警就说明信任链已建好。4.2 HTTPS页面里写死ws://请求还没出浏览器就被拦现象页面地址是https://example.com控制台报Mixed Content: The page at https://... was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ws://...连接直接被拒。原因浏览器主动升级策略禁止HTTPS页面发起明文WebSocket这属于主动混合内容比普通图片的被动混合内容拦截得更狠。解决不要在任何地方写死ws://用上一章的location.protocol三目运算动态拼协议。接手旧项目时全局搜一遍ws://搜索引擎收录的旧代码里写死协议的特别多。如果项目有PC端、H5多个入口把拼接协议的函数提取成公共模块复用别在三个文件里各写一份。4.3 请求握手指向426 Upgrade Required现象浏览器或调试工具里看到HTTP状态码426 Upgrade Requirednginx access log里出现一长串426。原因426是服务端明确告诉你“这个请求必须走Upgrade”通常有三种来源一是nginx配置漏了proxy_set_header Upgrade $http_upgrade或Connection二是proxy_http_version没设成1.1三是proxy_pass指向了一个普通HTTP服务端口那个端口根本不认Upgrade头。解决先对照第3.2节的配置逐行核对再确认proxy_pass指向的端口确实是WebSocket服务在监听。可以用第4.5节的curl命令直接打后端节点如果绕过nginx能返回101而经nginx返回426问题就锁定在nginx代理头上。4.4 101握手成功几十秒后连接1006秒断现象wss握手成功DevTools能看到101 Switching Protocols但连接在几十秒后被关掉前端收到1006 Abnormal Closure紧接着一连串自动重连。原因1006表示连接非正常关闭最常见的是nginx默认proxy_read_timeout 60s在作怪。WebSocket连接在60秒内没有任何数据帧经过nginxnginx就认为空闲并回收。如果你设了心跳但间隔恰好大于60秒或者压根没做心跳就一定会在60秒附近断。解决把proxy_read_timeout和proxy_send_timeout调到大于心跳周期的值我建议心跳间隔25到30秒nginx超时3600秒。如果前端还有多台后端组成负载均衡upstream必须做会话保持否则下一次请求被轮询到别的节点内存里的会话状态对不上连接照样被踢。最小配置是ip_hashupstream ws_backend { ip_hash; server 10.0.0.2:8080; server 10.0.0.3:8080; }ip_hash按客户端IP做哈希保证同一客户端固定落到同一个后端节点。它对NAT出口统一的大内网场景效果有限更严谨的是用sticky cookie但中小项目ip_hash够用。4.5 服务器上连不通安全组、证书链与openssl兜底排查现象本地一切正常部署到服务器就timeout或拒绝连接浏览器停在TLS握手阶段转圈。原因三层都有嫌疑——云服务器安全组没放行443端口、宿主机防火墙拦截、证书链不完整导致TLS握手校验失败。解决先确认端口监听正常ss -lntp | grep 443看到LISTEN且进程是nginx再用openssl验证证书链openssl s_client -connect ws.example.com:443 -servername ws.example.com /dev/null 21 | grep -E Verify return code|subject|issuer输出Verify return code: 0 (ok)说明证书链路没问题服务端TLS正常。接着查安全组和防火墙firewall-cmd --list-all最后用curl模拟一次WebSocket握手测整条链路curl -vk https://ws.example.com/ws \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ返回101 Switching Protocols说明握手链路全通返回4xx或5xx去看nginx的access.log和error.log通常直接就能定位是证书、路由还是Upgrade头的问题。这套命令组合我在现场排查wss问题时反复用比在浏览器里猜快得多。5. 让wss不掉线心跳机制实现与断线重连的落地技巧5.1 心跳间隔、nginx超时与网络NAT时间的三角关系wss建连后如果没有业务消息TCP层自带的keepalive默认周期太长移动网络的NAT和云负载均衡往往在几十秒到几分钟内就回收空闲连接。所以WebSocket长连接必须做应用层心跳这不是锦上添花是保命。三个参数要联动心跳间隔设在25到30秒小于移动网络NAT典型回收时间的一半nginx的proxy_read_timeout设在心跳间隔的三倍以上云负载均衡的idle timeout同样要查有些云厂商默认只有60秒不改就白搭。5.2 前端心跳与指数退避重连的最小实现心跳消息放在onopen里启动定时器每30秒发一次ping服务端回pong就算活着const WS_URL (location.protocol https: ? wss:// : ws://) location.host /ws; let ws null; let retryCount 0; let heartbeatTimer null; function sendHeartbeat() { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping, ts: Date.now() })); } } function connect() { ws new WebSocket(WS_URL); ws.onopen () { retryCount 0; heartbeatTimer setInterval(sendHeartbeat, 30000); }; ws.onmessage (e) { const msg JSON.parse(e.data); if (msg.type pong) return; // 业务消息处理 }; ws.onclose () { clearInterval(heartbeatTimer); const delay Math.min(1000 * 2 ** retryCount, 30000); retryCount 1; setTimeout(connect, delay); }; } connect();断线重连用指数退避首次失败等1秒之后2秒、4秒、8秒翻倍封顶30秒避免服务端刚出问题时几百个客户端同时重连把恢复之路堵死。服务端收到ping型消息回一条pong型消息这个动作轻量但能让nginx和负载均衡都感知到数据流避免空闲回收。5.3 用DevTools和日志验证心跳是否真在工作验证心跳生效有个直接的办法打开Chrome DevTools的Network面板切到WS标签观察收发帧的时间戳。如果30秒左右能看到一次ping和pong帧交替说明客户端到服务端整条链路都在工作。同时看nginx访问日志长连接在日志里的体现是101之后长时间不再产生新的请求记录只有手动断开才出现下一次连接。我自己的做法是断网几秒再恢复看客户端重连是否符合指数退避的预期延时这个测试能同时验证重连逻辑和心跳状态清理是否干净。最后说一条我踩过的血泪经验曾经把proxy_read_timeout留在nginx默认值结果线上所有wss连接平均65秒集体断开客户端无感重倒是好的但一个每隔一段时间就断一次的长连接等于没做。后来把心跳定在30秒、nginx超时定在3600秒、负载均衡idle timeout同步检查再没出现过批量掉线。wss配置本身不难难的是你意识到TLS、代理超时、心跳、负载均衡这几层是互相牵连的。每个参数背后都对应一个真实故障把第4章那5个场景提前过一遍你上wss会比别人省下大量夜里被叫醒的时间。希望帮到你。本文还有配套的精品资源点击获取