简介在Spring Boot 2.1中配置WebSocket并启用wss访问的完整工程示例面向需要为聊天室、实时推送、在线协作或股票行情等场景增加加密通道的Java后端开发者。它直接解决浏览器对明文WebSocket的限制以及HTTP服务在安全要求下无法直接升级为wss的问题让实时双向通信能够运行在HTTPS之上。压缩包共66个文件整体约61KB里面既有Java源码、XML配置、jks证书文件、properties属性配置也包含Maven包装脚本和完整的Git仓库元数据目录结构清晰便于对照自身项目调整。该资源已有13111人学习下载在同类配置案例中具备较高参考热度。示例从依赖引入开始依次演示keytool生成自签名证书、Spring Boot的SSL连接器配置、WebSocket处理器注册、前端JavaScript连接wss地址的完整链路同时兼容SockJS并针对证书配置错误给出了排错思路读者可将其快速迁移到自己的HTTPS项目中也能通过Git历史回溯每次配置改动从而深入理解WebSocket与TLS握手背后的原理。1. 为什么 ws:// 一部署就翻车WebSocket 配置 wss 访问要解决的三个先决问题每次把 WebSocket 服务从开发机搬到服务器都会收到同一类求助前端控制台飘红后端日志却干净得可疑。多数时候问题出在协议上——页面已经变成 HTTPS代码里还写着 ws://。浏览器的混合内容安全策略会直接拒绝这种连接的建立业务逻辑还没执行请求就死在了第一跳上。WebSocket 配置 wss 访问核心价值是把这条链路的四个环节理清楚TLS 终结放在哪、反向代理怎么写升级转发、证书链怎么接、超时与心跳怎么设置。这篇笔记适合部署长连接服务的后端开发与运维你不需要读完整的协议说明照着下面的配置和排查顺序就能把一条 wss 链路部署上线并确认它能长期稳定运行。2. WS 换成 wss 的本质协议升级、TLS 终结与反向代理的选型逻辑2.1 浏览器为什么拒绝 ws安全上下文与混合内容策略WebSocket 在浏览器里其实不是“随便连一个 TCP 端口”那么简单。页面通过 HTTPS 加载时浏览器把这个页面放在一个安全上下文里此时再发起 ws:// 连接属于主动混合内容因为页面本身是加密的而这条 WebSocket 链路却是明文。浏览器的态度很明确宁可连接失败也不让你在加密页面里偷偷跑明文通道。我见过很多项目在这个阶段踩坑报错文案常常是Failed to construct WebSocket后面跟着一句“Please use wss:// scheme”有的旧版本浏览器甚至只给一个红色错误连原因都不提示。另一个容易被忽略的风险在公网链路上。ws:// 的流量在传输过程中是明文当 WebSocket 帧里携带的是在线消息、客服会话或者设备轨迹这类高频数据时中间链路完全有能力记录和改写内容。wss:// 解决的正是浏览器到 TLS 终结点之间的传输安全问题它不需要应用层再做一次加密只要跑在 wss 通道里握手之后的所有帧都是密文传输。从协议设计看wss 并没有发明一套新协议它复用了 WebSocket 的握手流程只是把传输层套进了 TLS。所以浏览器开发者工具里看到的 wss 请求依然是一个带Upgrade: websocket头的 HTTP GET 请求证书验证、SNI 等流程和 HTTPS 完全一致。搞清楚这一点就不会在配置时把 wss 当成某种独立协议去折腾了。2.2 一次 wss 握手发生了什么GET、Upgrade 与 101 Switching ProtocolsWebSocket 的握手在 HTTP 语义里只是一个 GET 请求头部带了三个关键信息Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key。服务端如果接受这个升级请求会返回101 Switching Protocols随后这条 TCP 连接直接变成 WebSocket 数据通道不再按 HTTP 请求一问一答。之后两边就是双工通信谁都可以主动发数据控制面和数据面都在同一条连接里。问题往往出在反向代理这一层。常规的 HTTP 代理会丢弃 Upgrade 头部因为这种东西在普通 HTTP 请求里没有意义更严格的一些安全设备还会拦掉Connection: Upgrade。所以 wss 配置的核心动作就是让 Nginx 能把这类请求识别出来、原样转发并且在握手完成后保持连接长驻不能让代理把这条连接当成普通 HTTP 请求来处理。这里有个实际的部署结论如果当前是用 TCP 四层转发直接到达后端 WebSocket 服务TLS 终结要么在后端做要么由四层负载均衡整体透传。此时后端代码里如果做了基于来源地址或来源协议的判断拿到的都是负载均衡的地址排查问题时很容易绕弯路。这也是我倾向于把 wss 收敛到一个七层网关上的原因Nginx 对 WebSocket 升级的支持已经很成熟没必要自己维护一套 TLS 转发。2.3 TLS 终结放在哪一层反向代理与应用服务直挂证书“TLS 终结”指的是 TLS 加密握手到此结束之后的流量变成内部协议。常见做法有两个方向反向代理终结和应用服务终结我列出对比供选型时参考。对比维度反向代理终结应用服务终结证书管理在网关统一管理多服务共用一张证书每个服务单独配置证书与续期后端改造量后端保持 ws:// 监听代码基本不动框架需要配置 TLS 路径与版本排查难度后端本地就能用 ws:// 直接抓包调试线上与本地网络环境差异较大适用场景多服务共用域名、端口的前后端分离架构边缘计算、嵌入式、独立网关部署我的默认选择是反向代理终结。理由很直接长连接服务数量一多证书续期集中在网关一处不会因为某个后端服务忘了续期让全局 wss 失联后端可以继续用 ws:// 监听本地开发环境也不需要复刻一套正式证书。反向代理终结并不是没有代价它引入了一个代理层空闲超时、读取超时都要和业务心跳机制对齐多实例部署时还得处理会话粘连问题这两点正是后面避坑章节的主要内容。规模很小的项目也可以让后端直接终结 TLS少一层转发。但为了省掉一跳去牺牲证书统一管理我一般不建议这么干尤其是还有 Web 页面要共用一个域名的时候。2.4 路径与端口规划443 一条路还是单独端口吃流量路径规划几乎决定后续所有配置写法。如果 WebSocket 和 HTTPS 页面共用同一个域名通常走wss://域名/ws这样的统一前缀和普通 API 共享 443 端口。这样做的好处很明显证书只需一份防火墙只需要放行 443浏览器也不会因为跨端口产生额外的同源判断问题。也有人习惯单独开一个端口比如 8081 或 8443 专门给 WebSocket 用。这会让流量审计更清晰但代价也明显安全组要额外放行端口部分企业网络会拦截非标准端口的长连接用户连不上以后还很难定位。另开端口只适合后端集群完全独立、需要按端口做流量隔离的场景。我在配置时习惯给 WebSocket 单独划一个路径前缀比如/socket避免和业务 API 的/api路径混在同一个 location 里。这样做的另一个好处是后续要调整心跳、限流或者校验逻辑时只改这个 location不牵连普通 HTTP 路由。如果你要兼容多个端比如 App 端和 Web 端走不同握手参数路径前缀拆开以后也好做差异化处理。3. 用 Nginx 配置 wss证书准备、升级转发与超时参数的落地写法3.1 证书准备自签调试与正式证书的文件组织wss 连接能不能建立一半取决于证书链。内网或者本地调试阶段没必要为每个测试环境申请正式证书用 OpenSSL 生成一张自签证书就够了openssl req -x509 -newkey rsa:2048 \ -keyout /etc/nginx/cert/ws.example.com/key.pem \ -out /etc/nginx/cert/ws.example.com/cert.pem \ -days 365 -nodes \ -subj /CNws.example.com参数含义req -x509生成自签证书跳过 CSR 申请流程适合内网验证。2048RSA 密钥位数现在不建议低于 2048。days 365有效期一年自签证书建议短周期到期前记得重新生成。-nodes私钥不设密码启动 Nginx 时不用手工输入方便自动化重启。CN填写域名而不是 IP。如果要在浏览器里直接访问 IP 的 wss还需要在生成时加 SAN 扩展否则证书校验过不了。正式环境建议按域名建目录组织文件避免多套服务混在一起/etc/nginx/cert/ws.example.com/ ├── cert.pem ├── chain.pem ├── fullchain.pem └── key.pem上面fullchain.pem是站点证书加上中间证书的合并文件Nginx 的ssl_certificate应该指向它而不是只填站点证书。证书链缺失的症状很难一眼定位桌面浏览器可能一切正常手机端弱网或特定客户端直接报证书错误这个问题会在避坑章节里再展开。3.2 一份可以直接改域名使用的 Nginx 配置把 WebSocket 服务接到 wss 访问常见做法是走 Nginx 七层转发。下面是一份最小可用且能直接复制的配置只保留了与 WebSocket 相关的部分# 放在 http{} 顶层负责把客户端 Connection 头映射成合适的值 map $http_upgrade $connection_upgrade { default upgrade; close; } upstream ws_cluster { server 127.0.0.1:8080; keepalive 32; } server { listen 443 ssl; server_name ws.example.com; ssl_certificate /etc/nginx/cert/ws.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/cert/ws.example.com/key.pem; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1h; location /ws { proxy_pass http://ws_cluster; 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_connect_timeout 15s; proxy_read_timeout 60s; proxy_send_timeout 60s; } }关键行说明map块负责把$http_upgrade映射成$connection_upgrade。客户端请求里带了Upgrade: websocket时转发给后端的Connection头是upgrade如果只是普通 HTTP 请求则映射为close不会误伤常规 API 连接。proxy_http_version 1.1是 wss 配置里最容易漏的一行。WebSocket 握手依赖 HTTP/1.1 的持久连接语义如果仍然使用默认的 1.0后端会直接拒绝升级。proxy_set_header Upgrade与Connection两行必须配合 map 使用绝不能把 Connection 写死为upgrade否则所有经过这个 location 的普通 HTTP 请求也会被强制设为长连接连接数很快被打爆。keepalive 32让 Nginx 与后端之间保持一批复用连接减少频繁握手造成的延迟和 TIME_WAIT 堆积。这个指令只有在proxy_http_version 1.1时才有意义。proxy_read_timeout对长连接的意义最大。它表示从后端读取数据的空闲超时时间默认 60 秒。如果业务心跳周期超过 60 秒连接会被 Nginx 断开这就是很多线上 wss“准时断线”的来源。如果 WebSocket 路径不是/ws比如是/socket.io只需把location /ws改成对应前缀即可。注意proxy_pass后面不要带 URI 路径否则 Nginx 会对 location 匹配到的部分做路径重写后端实际收到的路径可能比预期少一段导致路由 404。3.3 把真实客户端信息传给后端X-Forwarded-* 头的作用配置里那三行X-Forwarded-For、X-Real-IP、X-Forwarded-Proto在日常开发中常常被忽略但一旦要做按 IP 限流或者按协议判断来源它们就发挥作用了。WebSocket 握手发生在 HTTP 请求阶段这些头在握手时已经完整传给后端后端在收到 Upgrade 请求时读取这些头就能拿到真实客户端 IP而不是反代的内网地址。需要注意直接把前端传来的X-Forwarded-For当真实 IP 是有风险的。更稳妥的做法是在 Nginx 层用real_ip模块把可信来源的X-Forwarded-For替换成$remote_addr再统一写入转发头。如果后端代码里写好了取 IP 的逻辑也要检查它取的是握手阶段的信息而不是等连接建立后再从 socket 上拿因为 WebSocket 建立以后根本没有 HTTP header 可读。X-Forwarded-Proto的作用是告诉后端原始请求是 http 还是 https。后端拿到这个头才知道页面是安全的从而决定构造用于回跳的 URL 时用https://还是http://否则很容易生成一个错误的 http 链接又引发一轮混合内容报错。3.4 别让防火墙在最后一米拦截端口与安全组检查配置完成后第一个动作不是打开前端页面而是确认网络路径上的端口是通的。443 端口在多数情况下已放行但如果你的 wss 用了非标准端口就要在安全组和防火墙两侧都加放行规则。可以这样检查本机监听状态ss -lnt | grep -E :(443|8080)\b如果 443 端口正常监听再用另一台机器用连接工具测 TCP 通断。如果端口通但握手失败问题在 Nginx 配置如果端口都不通优先检查安全组规则而不是反复折腾证书。另一个常见现象是 Nginx 配了 443后端服务也在同一台机器监听 443端口冲突导致 Nginx 启动失败。ss -lnt输出里同一个端口只能被一个进程监听看到两个进程争抢时先处理端口占用。4. 验证 wss 是否真的通了浏览器、curl 与 Node 客户端的三层检查4.1 浏览器控制台快速冒烟先看 readyState 再看状态码配置生效后的第一关是浏览器。打开业务页面进入开发者工具切到 Network 面板下的 WS 标签过滤所有 WebSocket 请求然后直接执行一段冒烟代码// 浏览器控制台直接粘贴 const ws new WebSocket(wss://ws.example.com/ws); ws.onopen () console.log(open:, ws.readyState); ws.onerror e console.error(error:, e.message); ws.onclose ev console.log(close:, ev.code, ev.reason);判断要点readyState为 1 表示握手成功连接可用。0 表示正在连接2 表示正在关闭3 表示已关闭。onerror里的事件对象信息有限很多浏览器拿不到真正的报错原因。此时要到 Network 面板的 WS 请求里看状态码。状态码101 Switching Protocols是唯一正确的握手结果。看到 403多半是跨域 Origin 校验失败看到 400是 Upgrade/Connection 头配置不对看到 502是 Nginx 连不上后端检查 upstream 配置和后端进程。浏览器验证的好处是贴近用户真实环境问题在于面板里不会展示证书链详情。所以只靠浏览器还不够下面用 curl 和 Node 客户端把链路拆开验证。4.2 用 curl 验证 TLS 和协议升级是否同时成立curl 不能真正完成 WebSocket 数据帧收发但用来验证握手阶段非常好用curl -i -N --insecure \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ \ -H Sec-WebSocket-Version: 13 \ https://ws.example.com/ws执行后重点看两处响应头里的HTTP/1.1 101 Switching Protocols以及Sec-WebSocket-Accept字段。只要看到 101说明从 Nginx 到后端这条升级链路是通的。参数说明-i输出响应头-N禁止缓冲连接建立后保持打开状态。--insecure是临时跳过证书校验的开关。生产验证时不要带它让 curl 走系统受信任证书才能顺便检查证书链是否完整。Sec-WebSocket-Key只需要是一个合法的 Base64 占位值多数后端实现并不严格校验这个值本身响应里的Sec-WebSocket-Accept才是服务端计算后的结果。如果 curl 返回 400优先检查 Nginx 的proxy_http_version和 Upgrade/Connection 头。如果返回 502说明后端连接不上重点查 upstream 端口与后端服务状态。4.3 完整收发一帧数据wscat 与 Node.js 客户端curl 只能证明握手成功数据帧收发还得用完整客户端。我常用的工具是 wscatnpm install -g wscat wscat -c wss://ws.example.com/ws连接成功后wscat 进入交互模式可以直接输入文本发送给服务端收到的响应也会实时打印。这个工具对排查“服务端有没有返回数据”非常直观比写完整脚本快得多。如果要把验证写进自动化流程可以用 Node 的 ws 库const WebSocket require(ws); const ws new WebSocket(wss://ws.example.com/ws, { rejectUnauthorized: false, // 仅自签测试环境使用生产删除 headers: { Origin: https://your-page.example.com // 模拟浏览器同源场景 } }); ws.on(open, () { console.log(opened); ws.send(JSON.stringify({ type: ping })); }); ws.on(message, data { console.log(message:, data.toString()); ws.close(); }); ws.on(close, (code, reason) { console.log(closed:, code, reason.toString()); });代码说明rejectUnauthorized: false只在自签证书的测试环境里开。生产环境必须略去该参数或者传入自定义 CA否则测试结果会掩盖真实的证书链问题。Origin头用来模拟浏览器行为。很多后端框架在 WebSocket 握手时会校验 Origin不带这个头可能被拒绝。这里填的域名要和生产页面保持一致。4.4 把握手验证固化进发布流程部署完 wss 配置后最容易被忽略的是“配置改好了但忘记 reload Nginx”这种情况。把握手验证写成一个脚本放入发布流程能从源头规避这种低级事故code$(curl -s -o /dev/null -w %{http_code} \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ \ -H Sec-WebSocket-Version: 13 \ https://ws.example.com/ws) if [ $code -ne 101 ]; then echo WSS handshake failed: $code exit 1 fi这里用-s静默模式避免输出多余信息-o /dev/null丢弃响应体-w %{http_code}只提取状态码。发布脚本里拿到 101 才继续否则直接失败能拦截大部分配置漏 reload、证书路径写错、端口没放行一类问题。5. wss 配置避坑记录混合内容来源、证书链顺序、空闲超时与多实例粘连5.1 现象浏览器提示连接失败Nginx 日志里什么都没有这是一个非常有代表性的现象。前端页面用 HTTPS 打开代码却照搬了开发环境的 ws:// 地址。浏览器在发出连接前就把请求拦掉了所以服务端访问日志里根本不会出现这条请求。原因浏览器强制要求安全上下文里的 WebSocket 必须使用 wss 协议ws:// 在 HTTPS 页面下被当作混合内容直接拒绝。解决不要在前端代码里写死协议而是根据页面协议动态拼接const wsProto location.protocol https: ? wss:// : ws://; const ws new WebSocket(${wsProto}${location.host}/ws);这里用location.host而不是location.hostname目的是把端口也带上避免页面运行在非 443 端口时WebSocket 请求打到错误的端口上。开发环境用 http 时自动走 ws线上 https 时自动走 wss一套代码两端通用后面再也不会因为协议写死而出问题。5.2 现象桌面浏览器正常手机端却提示证书无效桌面端浏览器对证书链的宽容度较高但部分手机浏览器或旧客户端对证书链要求更严格。表现是 wss 握手卡住重试多次仍失败。原因Nginx 的ssl_certificate只配置了站点证书没有把中间证书合并进去。客户端只信任它本地内置的根证书缺少中间证书时无法完成信任链验证。解决把站点证书和中间证书按顺序合并成一份文件顺序必须是“站点证书在前中间证书在后”cat ws.cert.pem chain.pem fullchain.pem合并后用 openssl 检查服务端证书链openssl s_client -connect ws.example.com:443 -servername ws.example.com /dev/null | grep verify看到verify return code: 0 (ok)说明证书链完整。如果看到unable to get local issuer certificate说明中间证书还是没发出去继续检查合并文件内容不要只对比文件名是否存在。5.3 现象wss 连接能建立但约 60 秒后准时断开这是部署 wss 时最高频的线上问题用户反馈通常是“连上之后一分钟左右就掉线重连以后也只能撑一分钟”。原因Nginx 的proxy_read_timeout默认值是 60 秒。当 WebSocket 连接处于空闲状态、没有数据帧经过代理时Nginx 会把连接判定为超时并主动关闭。如果业务应用没有做心跳保活断线时间会非常精确地落在 60 秒附近。解决从两个方向入手。先调整代理超时让代理层与业务心跳周期对齐location /ws { proxy_read_timeout 75s; proxy_send_timeout 60s; }更根本的措施是给应用加上心跳帧。WebSocket 协议本身就支持 ping/pong 帧前端或后端每 25 到 30 秒发送一个 ping对端返回 pong代理层会持续看到数据流动不会触发空闲超时。只调大代理超时是治标不治本长连接服务一旦没有心跳遇到负载均衡节点切换或网络抖动时仍然会被静默断开。5.4 现象单实例正常多实例部署后 wss 频繁掉线如果后端服务从一台扩到多台wss 连接反而开始不稳定浏览器反复重连每次重连只能撑几秒到几十秒不等。原因WebSocket 是有状态的连接连接建立后所有消息都在这条 TCP 通道上传输。负载均衡把握手请求分到实例 A后续数据帧却被算法分到实例 B两个实例各自维护的连接状态不同步消息自然传递不过去。解决思路有两类。第一类是让网关做会话粘连把同一个连接固定在同一个后端实例上。Nginx 的ip_hash对固定出口的办公网络简单有效但移动网络会频繁切换出口 IPip_hash 不一定稳定更通用的是启用负载均衡的会话保持或 cookie 粘滞能力。第二类是后端把连接会话集中管理比如把连接 ID 与节点映射放到共享缓存收到消息后转给持有对应连接的节点处理。这个方案改造量稍大但在多实例架构里最可靠。5.5 现象握手返回 403业务组件看起来都正常浏览器 Network 面板里能看到这条 wss 请求但状态码是 403服务端日志提示不是一个被允许的来源。原因服务端框架对 WebSocket 握手做了同源校验握手请求里的 Origin 头不在允许列表。这个问题和 Nginx 无关是后端业务代码的跨域白名单没有覆盖生产页面域名。解决把生产页面域名加进服务端允许来源配置。比如页面部署在https://your-page.example.com就把这个域名加到 WebSocket 服务的跨域白名单中。调试时可以用上一章 Node 客户端的 Origin 头模拟这个来源不需要每次改完都去页面刷新验证。要注意 Origin 头里带的是协议加域名加端口配置白名单时尽量精确到这种完整格式避免为了省事用通配符放开所有来源。6. 生产环境最后一公里wss 心跳节奏、负载均衡连接与断线重连6.1 让心跳、代理超时和负载均衡空闲阈值保持同一节奏wss 配置一旦跑起来长期稳定性靠的是对齐三组时间应用心跳间隔、反向代理读超时、负载均衡空闲阈值。三者的理想关系是“心跳间隔最小代理超时居中负载均衡空闲阈值最大”。我常用的基准值如下项目推荐值心跳间隔25 秒Nginx read_timeout75 秒前端重连初始延迟1 秒前端重连最大延迟30 秒心跳间隔不建议压到 10 秒以内过于密集的 ping 帧会浪费移动网络电量和带宽比 25 秒更长则容易逼近网关空闲阈值。如果负载均衡的空闲超时无法调整就把心跳间隔压到该阈值的二分之一甚至三分之一。6.2 断线重连的正确姿势指数退避与关闭码区分很多前端的重连逻辑就是简单写一个固定延迟的setTimeout(reconnect, 3000)服务端一重启几千个客户端在同一秒发起 wss 握手网关和后端同时承压。正确的姿势是加指数退避let retryDelay 1000; const maxDelay 30000; let manualClose false; function connect() { const ws new WebSocket(wss://ws.example.com/ws); ws.onopen () { retryDelay 1000; // 成功后重置重试延迟 }; ws.onclose (e) { if (manualClose) return; setTimeout(connect, retryDelay); retryDelay Math.min(retryDelay * 2, maxDelay); }; } connect();这段代码做了三件事成功后把等待时间复位、断线后延迟翻倍、限制最大延迟。关键点是manualClose这个标志位——当页面主动关闭连接或用户退出登录时要把manualClose置为 true否则用户退出后还会无限重连骚扰服务器。另外关闭码也很重要收到 1008 这类策略拒绝码说明业务层面不允许连接这时候不应该继续重连而是提示用户重新登录。我的习惯是把关闭码分成“可自动重连”和“需要人工介入”两类而不是所有 close 都走同一套延迟策略这样既保证恢复速度又不会在业务拒绝时制造海量无效连接。到现在wss 的部署才真正从“能连上”变成“能长期跑”。我每次上线长连接服务都会把握手验证、心跳节奏、断线重连三项过一遍检查单这三项帮我挡住了绝大多数用户在凌晨才发现的事故。希望这份配置和排查顺序能帮到你。本文还有配套的精品资源点击获取