简介面向需要为WebSocket服务启用安全wss通道的Spring Boot开发者这份压缩包基于Spring Boot 2.1项目完整演示从添加依赖、配置SSL证书到注册WebSocket处理器、前端连接wss地址的落地过程适合正在解决HTTPS下实时通信问题的中高级后端与全栈工程师。包内共66个文件包含Java源码、Maven的pom.xml配置、sample示例文件、jks证书文件以及IDE与Git仓库相关配置整体约61KB目录结构清晰便于直接对照修改。压缩包还保留了完整项目目录与Git元数据可帮助看清配置改动脉络。已有超过1.3万人浏览学习内容覆盖生产环境常用的安全配置思路既给出Tomcat容器中keystore的加载方式也提供SockJS回退与文本消息处理示例可帮助读者快速跑通wss访问链路减少排查证书与握手问题的时间。1. wss 不是可选项线上 ws 服务被浏览器拦截之后我第一次把 WebSocket 服务部署到公网时8080 端口跑得好好的前端用 ws:// 调试也一直正常。等页面切到 HTTPS 之后Chrome 控制台一夜之间全是红色报错连接全部失败。那时才反应过来webSocket 配置 wss 访问这步没做。wss 本质就是 WebSocket over TLS把明文 ws 通道放进证书加密链路里否则 HTTPS 页面根本不让你连。这篇文章写给那些域名和证书都准备好、正要给 WebSocket 服务接通公网的开发者内容覆盖 wss 的握手原理、Nginx 反代和后端直连两种落地配置、以及上线后最常踩的坑照着做就能让 websocket js 客户端稳定连上 wss。2. 先吃透握手链路ws 到 wss 不只是多加一个字母很多人改配置时只把ws://换成wss://甚至把端口从 80 换成 443然后发现仍然连不上。原因是不清楚 WebSocket 的握手过程wss 不是简单的“安全版 URL”而是在协议栈上先走 TLS再走 WebSocket 握手。这一章把链路拆开后面不管是配 Nginx 还是直连后端遇到报错你都能快速判断是哪一段出了问题。2.1 一次 WebSocket 握手从 HTTP Upgrade 到 101 Switching ProtocolsWebSocket 握手是建立在 HTTP 之上的。客户端发一个 GET 请求带着 Upgrade 头告诉服务器“我想升级协议”。服务器如果允许返回 101 Switching Protocols之后这条 TCP 连接上不再走 HTTP 语义转而收发 WebSocket 帧。GET /ws HTTP/1.1 Host: chat.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13 Origin: https://chat.example.com HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo这里有两个字段值得注意。Sec-WebSocket-Key是客户端生成的随机值服务端收到后与一个固定的 GUID 拼接做 SHA-1 再 Base64返回给Sec-WebSocket-Accept。它没有加密作用只是告诉客户端“我确实理解 WebSocket 握手”。Sec-WebSocket-Version: 13是目前所有浏览器都采用的标准版本如果你的服务端实现太老或写死了其他版本握手会直接失败。理解这段握手对排查 wss 问题很关键。很多人看到 400 Bad Request第一反应是证书或防火墙其实先去看 Nginx 日志和后端日志里有没有 Upgrade 头、Sec-WebSocket-Key 是否为空往往一分钟就能定位。合法的握手请求必须同时满足GET 方法、HTTP/1.1 以上、Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Version: 13。要注意 Host 和 Origin 头在反代时会影响业务判断——Nginx 加上proxy_set_header Host $host;才能把原始 Host 传给后端后端拿 Host 做多域名路由时漏配就会出错。2.2 wss 等于 WebSocket over TLS多出来的一次 TLS 握手wss 的协议栈是TCP → TLS → HTTP Upgrade → WebSocket 帧。TLS 握手发生在 WebSocket 握手之前也就是说客户端先验证服务器证书、协商对称密钥再发送上面的 Upgrade 请求。证书不可信时浏览器会在 TLS 阶段直接报错你根本看不到 101。和 ws 相比wss 多出的开销主要是首次连接时的 TLS 握手。TLS 1.3 下需要一次往返1-RTTTLS 1.2 需要更多次。对实时性要求高的场景比如行情推送、白板协作建议在 Nginx 上开启ssl_session_cache和ssl_session_tickets来减少重复握手开销这个参数在后面的配置里会再次出现。wss 的首次连接看着比 ws 慢其实慢在 TLS 协商而不是 WebSocket 本身。还有一个容易忽略的事实WebSocket 帧本身不加密。即使你用 ws数据在 TCP 上也是明文抓包就能直接看到消息内容。wss 是把整条链路罩在 TLS 里面。所以聊天、支付通知、控制指令这类数据上 wss 不是合规要求问题是基本底线。你可以用 tcpdump 抓一次包直观感受ws 流量里能看到 JSON 原文wss 流量里只有 TLS 加密后的密文。2.3 三条落地路径怎么选反代、直连、网关常见做法有三条路选型不复杂但要先想清楚再动手。第一条是 Nginx 反向代理最常用。Nginx 监听 443 终结 TLS向内网后端转发时继续用 ws后端完全不用碰证书。nginx 代理服务器文件访问那套经验和规则可以直接复用多实例扩容时 upstream 也顺手解决了。适合新增一个 WebSocket 给已有公网域名的场景证书统一管理和现有 Web 体系共用。第二条是后端直连 wss。后端进程自己加载证书、自己监听端口Node.js 和 Python 都有成熟库支持。适合内网工具、单体服务或者你不想为一个小接口多维护一层 Nginx。缺点是证书换了要重启后端多实例时还得自己处理负载均衡。第三条是平台网关比如云负载均衡、API 网关、Kubernetes Ingress。大多数支持 WebSocket但有些默认不开启 Upgrade需要在注解或控制台里显式打开。这块和具体平台绑定配置方式各有差异我不展开。如果你选了这条路径务必先查平台文档确认能不能转发 Upgrade 头不要把平台当成透明转发。选型建议是公网域名已经挂在 Nginx 上的直接走反代只有内网部署、后端本来就单实例的再考虑直连。接下来两章分别把这两条路讲透。3. Nginx 反代搭建 wss一套能直接抄的配置绝大多数生产环境已经有 Nginx 在 443 端口服务 HTTPS 页面WebSocket 服务要和页面共用域名最省事的方式就是加一个 location 并转发 Upgrade 头。这一章给出一套能直接抄的配置并解释每个参数为什么这样设。3.1 证书准备PEM 链、私钥和常见出错点反代 wss 前要先有三样东西站点证书、中间证书、私钥。云厂商买的证书一般会给你一个 bundle 文件里面站证书和中间证书是分好的如果只有一张 .crt很可能缺中间证书。把站点证书和中间证书合并成一个 PEM 文件顺序是站点证书在前中间证书在后cat chat.example.com.crt intermediate.crt /etc/nginx/ssl/chat.example.com.pem cp chat.example.com.key /etc/nginx/ssl/chat.example.com.key chmod 600 /etc/nginx/ssl/chat.example.com.key合并后先验证一下证书内容和域名是否匹配openssl x509 -in /etc/nginx/ssl/chat.example.com.pem -noout -subject -dates -ext subjectAltName这个命令会打印证书的签发对象、有效期和域名列表。确认subjectAltName里包含你的聊天域名否则浏览器会报域名不匹配。另外注意私钥权限Nginx worker 进程通常是 www-data 或 nginx 用户私钥权限太开放会触发 SELinux 或系统安全策略的告警。为什么强调证书链因为移动端和 Android 原生客户端的信任库跟桌面 Chrome 不一样。只配站点证书不配中间证书桌面浏览器可能靠 OS 补全链侥幸连上手机端会大面积握手失败这是 wss 上线后最典型的隐形问题。合并时如果有多级中间证书全部按顺序拼在站点证书后面。3.2 Nginx 配置Upgrade 头、连接复用与超时语义先把 WebSocket 升级需要用到的 map 放进 http 块里注意它必须在 server 块外面map $http_upgrade $connection_upgrade { default upgrade; close; }这个 map 的作用是客户端请求带Upgrade: websocket时$http_upgrade不为空Connection头就被置为 upgrade如果客户端只是普通 HTTP 请求Connection头就是 close。避免你把 WebSocket 和普通接口放在同一个 location 时普通请求也被错误当成升级请求处理。然后是 server 块server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/nginx/ssl/chat.example.com.pem; ssl_certificate_key /etc/nginx/ssl/chat.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:WebSSL:10m; ssl_session_timeout 10m; 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; proxy_send_timeout 60s; proxy_buffering off; } }逐个说参数。proxy_http_version 1.1必须有HTTP/1.0 不支持 Upgrade 语义漏了它后端永远看不到升级请求。proxy_set_header Upgrade $http_upgrade和Connection $connection_upgrade是一对组合把客户端的升级意图原样传给后端。proxy_read_timeout 60s很多人理解错它的语义是“两次从上游读取数据之间的最大间隔”不是“连接最多活 60 秒”。所以客户端每 20 秒发一次心跳60 秒超时是安全的如果心跳间隔 30 秒而你设了 20 秒连接必断。proxy_buffering off对 WebSocket 这类双向推流很有用关闭缓冲能让后端推过来的数据立刻到客户端不会攒在 Nginx 缓冲区里。重启前先做语法检查nginx -t nginx -s reloadssl_session_cache shared:WebSSL:10m是给 TLS 会话复用用的对 wss 的场景影响很大。WebSocket 握手完成后TLS 会话如果被缓存后续短连接可以少一次握手对长连接场景影响不大但页面里同时有 HTTP 请求时整体 HTTPS 体验会明显提升。3.3 配置检查与本地联调怎么确认 wss 真的通了配完先别急着连客户端用 curl 手动触发一次 wss 握手这一步能快速定位问题在 Nginx、证书还是后端curl -i -N \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ \ https://chat.example.com/ws/预期返回HTTP/1.1 101 Switching Protocols。如果证书没配好curl 会直接报证书验证错误如果返回 502去看 Nginx error.log 和后端监听端口如果返回 400多半是 Upgrade 头没转发到后端。自签名证书做本地测试时可以加-k跳过证书校验但那只适合你本机联调生产环境不能靠它验收。再打开浏览器控制台跑一段 websocket js 客户端确认握手和收发消息都正常const ws new WebSocket(wss://chat.example.com/ws/); ws.addEventListener(open, () { console.log(wss open); ws.send(hello from browser); }); ws.addEventListener(message, (event) { console.log(recv:, event.data); }); ws.addEventListener(close, (event) { console.log(close:, event.code, event.reason); }); ws.addEventListener(error, (err) { console.error(ws error:, err); });浏览器开发者工具的 Network 面板里切到 WS 标签能看到握手帧状态为 101Message 帧里的收发内容也一清二楚。如果这里通了说明 Nginx 反代这条链路完全没问题。前端连不上时先跑一遍 curl 和这个脚本能少走很多弯路。4. 后端直连 wssNode.js 与 Python 两种写法反代不是唯一选择。内网服务、临时工具、或者你不想为了一个几 KB 的推送接口再维护一层 Nginx可以在后端直接终结 TLS。这一章给 Node.js 和 Python 两个最小可运行示例代码可以直接抄顺便说清楚直连和反代的取舍。4.1 Node.jshttps ws 库搭起最小 wss 服务先用npm install ws装好依赖然后新建 server.jsconst https require(node:https); const fs require(node:fs); const { WebSocketServer } require(ws); const options { cert: fs.readFileSync(/etc/nginx/ssl/chat.example.com.pem), key: fs.readFileSync(/etc/nginx/ssl/chat.example.com.key) }; const server https.createServer(options, (req, res) { res.writeHead(200, { Content-Type: text/plain }); res.end(wss server is running); }); const wss new WebSocketServer({ server }); wss.on(connection, (ws, req) { console.log(client connected:, req.socket.remoteAddress); ws.on(message, (data, isBinary) { if (!isBinary) { console.log(recv:, data.toString()); } ws.send(echo: ${data}); }); ws.on(close, () { console.log(client disconnected); }); ws.on(error, (err) { console.error(ws error:, err); }); }); server.listen(9443, () { console.log(wss listening on 9443); });这段代码的核心是new WebSocketServer({ server })它直接复用了已经挂好证书的 https.Server不需要手动监听 upgrade 事件。cert和key对应 Nginx 里那份 PEM 证书和私钥同一个文件可以两边共用。监听 9443 是为了避开 443 端口冲突如果你这台机器上没有别的 Web 服务也可以直接监听 443前端 URL 就不需要带端口。说明两个容易被忽略的点。第一ws.on(message, (data, isBinary) ...)里 data 可能是 Buffer 也可能是字符串直接拼接回显没问题但做业务解析时最好先判断 isBinary避免把二进制帧当文本解析。第二生产环境不要把证书路径和端口写死在代码里用环境变量传入多进程部署时用 PM2 或 Node cluster 管理每个 worker 都会持有一份证书文件句柄替换证书文件后要平滑重启。4.2 Pythonwebsockets 库直连 wssPython 侧用 websockets 库先pip install websockets。较新的库版本把导入路径调整成了websockets.asyncio.server老代码里看到的websockets.server也可以跑但新项目建议直接用新路径import asyncio import ssl from websockets.asyncio.server import serve async def echo(websocket): async for message in websocket: print(frecv: {message}) await websocket.send(fecho: {message}) ssl_context ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain( certfile/etc/nginx/ssl/chat.example.com.pem, keyfile/etc/nginx/ssl/chat.example.com.key, ) async def main(): async with serve(echo, 0.0.0.0, 9443, sslssl_context) as server: await server.serve_forever() if __name__ __main__: asyncio.run(main())和 Node 版本对照着看sslssl_context就是 ws 和 wss 的分界线不传这个参数就是明文 ws传了才是 wss。load_cert_chain只需要证书文件和私钥证书文件里可以包含完整的中间证书链Python 的 ssl 模块会按文件顺序解析。serve_forever()是 websockets 12.x 之后推荐的写法老版本里常见start_server加run_until_complete如果你跟着旧教程写过API 变了不要慌看下导入路径和函数签名即可。这个库的心跳机制实现也很合拍可以用await websocket.ping()发协议层的 Ping 帧服务端自动响应 Pong不需要业务层做字符串心跳。前提是客户端也要能处理协议 Ping——浏览器不暴露这个能力所以 Node 和 Python 之间互通没问题浏览器端还是要走业务心跳。4.3 直连和反代怎么选一次说清对比维度Nginx 反代后端直连证书更换只改 Nginxreload 即可每个后端实例替换文件并重启TLS 性能Nginx 的 OpenSSL 优化成熟依赖语言运行时Node 和 Python 差异不小多实例upstream 直接做负载均衡需要自己加负载层或前置代理链路排查多一层要拆两段定位少一个变量好查端口复用 443前端不带端口一般用独立端口前端要带适用场景公网、HTTPS 页面、多实例内网、单实例、临时工具我的习惯是后端越简单越好能丢给 Nginx 做的不要在后端代码里重复实现。直连适合的是“这台机器上没有 Nginx”或者“后端只想暴露一个端口给内网调用”的场合。另外要提醒一句直连 wss 如果开了防火墙记得放行对应端口比如 9443否则客户端连不上时你查半天证书最后发现是安全组没放行。5. wss 配置避坑清单5 个真实翻车现场与排查路径wss 配置表面上就是改几行 Nginx但线上翻车往往集中在几处。这一章按“现象 → 原因 → 解决”写五条我实际见过的高频问题每一条都能对上你的报错记录。5.1 现象一HTTPS 页面连 ws 被浏览器拦截Mixed Content现象页面已经通过 HTTPS 访问控制台出现红色报错内容是Mixed ContentWebSocket 连接根本没发出去。原因浏览器安全策略禁止 HTTPS 页面主动连明文 ws 或 http 接口。你后端服务其实活着只是浏览器不让前端连。解决前端根据当前页面协议动态选择 ws 还是 wssconst wsProtocol location.protocol https: ? wss: : ws:; const wsUrl ${wsProtocol}//chat.example.com/ws/; const ws new WebSocket(wsUrl);这样 HTTPS 页面自动走 wss本地 http 开发环境自动走 ws不需要每次上线改一遍 URL。同时还检查一下页面里有没有残留的http://资源引用图片、接口、脚本都算Chrome 对混合内容现在是越来越不给面子。5.2 现象二Nginx 反代返回 502Upgrade 头没传现象普通页面能打开WebSocket 一连就 502后端日志里看到的请求没有Upgrade头或者直接没有到达 WebSocket 处理器。原因location 里漏了proxy_set_header Upgrade $http_upgrade;和对应的 Connection 头Nginx 按普通 HTTP 请求把流量转发给后端后端不认。解决确认这段配置存在并且proxy_http_version 1.1也写上了proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;如果已经配了还 502去 Nginx error.log 看upstream ... connect() failed多半是proxy_pass指向的后端端口没起来或者写错地址。别被 502 带偏先确认 upstream 的 TCP 端口能通再回头查头。5.3 现象三101 握手成功但空闲后被断开现象连接刚建立时一切正常但只要消息静默超过一分钟客户端 onclose 就收到异常关闭代码可能是 1001 或 1006。原因Nginx 的proxy_read_timeout默认 60 秒如果后端在这段时间内没有向代理推送任何数据代理认为上游死了主动断开。解决把超时调大同时把心跳间隔和超时对齐proxy_read_timeout 300s; proxy_send_timeout 300s;但别天真以为把超时调到无限大就完事。云平台、运营商网关、公司出口防火墙都可能对空闲 TCP 连接下手中间设备断链时连接两端是感知不到的。所以真正可靠的做法是同时实现心跳机制实现客户端每 20 到 30 秒发一个业务心跳包服务端收到后回一个 pong。这样连接一直有数据流动任一层网关都不容易把它当死链清掉。心跳间隔必须小于所有超时参数建议取 Nginx 超时时间的四分之一到三分之一。5.4 现象四证书链不完整移动端大面积连不上现象桌面 Chrome 看起来一切正常但 Android、iOS 或某些原生客户端连不上报证书错误或直接握手失败。原因Nginx 配置里只放了一张站点证书没有把中间证书合进去。桌面浏览器可能从系统信任库自动补全链移动端信任库不全就断。解决重新合并证书链并重启 Nginxcat site.crt intermediate.crt site-fullchain.pem nginx -t nginx -s reload合并后用这个命令从客户端视角验证openssl s_client -connect chat.example.com:443 -servername chat.example.com /dev/null 21 | grep -E subject|issuer|verify看到verify return code: 0 (ok)才算过。如果显示unable to get local issuer certificate说明中间证书没配或者顺序反了。这个问题最容易出现在证书快到期换新证书的时候因为新证书的签发链可能和旧证书不一样只替换站点证书没替换链文件又会翻一次车。5.5 现象五多实例负载均衡导致连接错乱现象两个后端实例同时运行客户端 A 连接落在实例 1但消息推送走到了实例 2导致 A 收不到消息或者偶尔收到别人的消息。原因WebSocket 是有状态长连接Nginx 默认轮询会把不同请求发给不同后端升级前的一次握手和升级后的数据帧就分家了。解决upstream 加会话保持upstream ws_backend { ip_hash; server 127.0.0.1:8080 weight1; server 127.0.0.1:8081 weight1; } server { location /ws/ { proxy_pass http://ws_backend; # 其余 proxy_set_header 同上 } }ip_hash按客户端 IP 哈希到固定后端大部分场景管用。但移动网络出口 IP 会变化换基站就可能漂移。生产环境更可靠的方案是后端把连接信息放进 Redis 做广播或者让所有实例订阅同一个消息总线连接落在谁身上无所谓。这个问题在配置阶段就要想清楚等上线后出现串号再改负载策略动辄要重启所有连接代价很大。6. 配完 wss 后的最后一步心跳机制与断线重连wss 握手通过、浏览器显示绿色小锁这只是起点。公网长连接最大的敌人是“静默断开”链路空闲导致 Nginx 或云负载均衡超时或者运营商把空闲 TCP 连接悄悄回收。你需要的是一套心跳加重连机制让连接断了能自己站起来。6.1 心跳机制实现前后端各做一半浏览器 WebSocket API 没有暴露协议层 Ping 帧所以前端只能用业务消息模拟。常用的做法是约定一个特殊消息比如__ping__服务端收到后回__pong__function createClient() { const ws new WebSocket(wss://chat.example.com/ws/); let timer null; ws.onopen () { timer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(__ping__); } }, 20000); }; ws.onmessage (event) { if (event.data __pong__) { return; } // 在这里处理真实业务消息 }; ws.onclose () { clearInterval(timer); setTimeout(createClient, 3000); }; ws.onerror () { ws.close(); }; } createClient();心跳间隔 20 秒对应前面 Nginx 的proxy_read_timeout至少要留出三倍余量建议设 60 秒以上。后端也不要闲着在 Node.js 的 ws 库里可以调ws.ping()直接发协议层 Ping 帧Python websockets 库有await websocket.ping()。两端都发不是坏事但至少保证一端在持续试探链路质量。6.2 断线重连用指数退避避免风暴固定 3 秒重连看起来简单但如果晚上服务端重启成百上千个客户端同时掉线、同时重连服务端会在几秒内被打满。更稳妥的做法是指数退避第一次失败等 1 秒第二次 2 秒第三次 4 秒封顶 30 秒每次加一点随机抖动。let attempt 0; function connect() { const ws new WebSocket(wss://chat.example.com/ws/); ws.onopen () { attempt 0; }; ws.onclose () { const delay Math.min(30000, 1000 * Math.pow(2, attempt)) Math.random() * 1000; attempt 1; setTimeout(connect, delay); }; } connect();这个版本把onerror交给onclose处理因为 WebSocket 遇到错误后一定会触发 close两边都写反而容易重复重连。连接成功时把 attempt 清零下次断线从 1 秒重新起步。我最早给公司搭 wss 时配完 Nginx、用 curl 验证 101 通过就以为收工结果第二天用户反馈凌晨全部掉线——查了一圈是空闲超时把连接切了而前端压根没做重连。后来我把“心跳间隔、服务端超时、指数退避重连”写进每次上线 checklist再没出过这类离谱事故。wss 的坑大多不在 TLS 本身而在你默认它“已经通了”的那一刻。希望帮到你。本文还有配套的精品资源点击获取