简介:这是一份面向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套上TLS:wss配置这四步到底在做什么
你在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服务并在同一个端口挂WebSocket,Node.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就复用443,nginx同一监听端口下用location区分WebSocket路径和普通HTTP路径,代价最小。
证书方面要分清两个场景。公网环境用Let's 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 "/CN=ws.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里加rewrite:
location /ws { rewrite ^/ws(/.*)$ /echo$1 break; proxy_pass http://127.0.0.1:8080; }rewrite ^/ws(/.*)$ /echo$1 break把开头的/ws替换成/echo,break表示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交给nginx:
const 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_INVALID,wss连接根本没进入WebSocket握手阶段。原因:自签证书不在操作系统受信任根证书列表里,TLS握手阶段被浏览器判为无效,直接掐断。这不是配置写错,是信任链缺失。解决:内网联调时把自签证书的CA文件导入本机受信任根证书,Windows在证书管理器导入,macOS在钥匙串里设为始终信任,Linux放到/usr/local/share/ca-certificates后执行update-ca-certificates,导入后重启浏览器再连。公网环境老老实实用Let's 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 Required,nginx 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秒内没有任何数据帧经过nginx,nginx就认为空闲并回收。如果你设了心跳但间隔恰好大于60秒,或者压根没做心跳,就一定会在60秒附近断。解决:把proxy_read_timeout和proxy_send_timeout调到大于心跳周期的值,我建议心跳间隔25到30秒,nginx超时3600秒。如果前端还有多台后端组成负载均衡,upstream必须做会话保持,否则下一次请求被轮询到别的节点,内存里的会话状态对不上,连接照样被踢。最小配置是ip_hash:
upstream 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 2>&1 | 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会比别人省下大量夜里被叫醒的时间。希望帮到你。
本文还有配套的精品资源,点击获取