☰
WSS配置全指南:从证书到Nginx反向代理的WebSocket安全部署
2026/10/10 6:42:50 网站建设 项目流程

简介:面向希望为Spring Boot WebSocket服务启用安全HTTPS通道的Java开发者,这份资源提供了一个可直接运行的Spring Boot 2.1项目,覆盖通信原理说明与可编译代码,聚焦wss协议落地,适用于聊天室、实时推送、协同办公、在线客服等需要长期双向连接的场景。方案包含从JDK keytool生成自签名JKS证书,到在TomcatFactory中同时开放HTTP与HTTPS端口,再到注册WebSocket处理器并用SockJS兼容非标准浏览器的完整链路;前端JavaScript也给出连接wss地址、发送与接收消息的示例,后端则提供连接建立、消息回显等日志输出,便于验证握手是否成功。包内共66个文件,以Java类、XML配置、properties文件、keystore.jks证书、Maven wrapper脚本和IDE工程文件为主,整体压缩包约61KB,便于整包导入并对照排查。已有13111人浏览学习,说明这组配置对同样面对wss与证书问题的团队有借鉴价值。

1. 配置 WSS 访问:先搞懂 WebSocket 为什么被浏览器强制要求 TLS

WSS 访问指用 wss:// 协议替换 ws:// 来连接 WebSocket 服务的完整配置链路,涉及证书准备、反向代理转发、后端握手校验三个环节。很多开发者第一次做这块,是在把实时推送或聊天功能部署到生产环境之后:本地联调时 ws:// 一切正常,一旦页面走 HTTPS,浏览器控制台立刻出现 insecure WebSocket endpoint 的拦截报错,连接直接被安全策略掐断。此时唯一合规的路径就是让服务端支持 wss:// 访问,把 WebSocket 的握手请求放进 TLS 通道里。下面按证书、代理、后端、排查的顺序,把 wss 从申请到验收需要改的配置一次讲清,适合刚接触服务端 WebSocket 部署的开发者,也适合已经上线但被握手问题卡住的运维。

2. 证书准备与部署:自签名、免费证书与服务器上的证书路径

2.1 证书选型:自签名只配调试,生产环境必须用受信 CA

配置 wss 访问的第一步不是碰 Nginx,而是先把 TLS 证书准备好。证书决定浏览器是否信任这个连接,也决定了你后续所有排错的方向。我一般这样划分:内网联调、测试环境、开发机,直接用自签名证书,省去域名和 CA 申请环节,能快速验证代理配置和代码逻辑;面向公网的生产环境,用免费 CA 签发的证书,或者公司已有的企业证书,关键点是浏览器、安卓和 iOS 客户端都信任它。

为什么不在生产环境用自签名?因为自签名证书的信任根不在系统信任库里,浏览器会拦截整条连接,用户不可能每次访问都手动导入证书。你当然可以在客户端代码里跳过证书校验绕过,但这会让 wss 加密失去防篡改意义,而且很多原生 WebSocket 实现根本不给你关校验的开关。所以自签名只承担验证配置链路通不通的任务,生产环境的证书选型没有任何讨论空间。

2.2 用 OpenSSL 生成自签名证书:SAN 字段是 wss 握手的关键

自签名证书生成这一步踩坑率极高,大多数翻车都出在证书里没有 SAN(Subject Alternative Name)字段。TLS 握手时,客户端会用你访问的域名去匹配证书的 SAN 列表,证书里只有 CN(Common Name)是不够的。新版 Chrome 不再使用 CN 做域名校验,只认 SAN,没有 SAN 的证书无论 CN 怎么写都会被判定为域名不匹配。我生成自签名证书时,会把域名和 IP 都写进 SAN,避免换访问方式又报错。

openssl req -x509 -newkey rsa:2048 -nodes \ -keyout wss.key -out wss.crt \ -days 365 \ -subj "/CN=chat.example.com" \ -addext "subjectAltName=DNS:chat.example.com,DNS:localhost,IP:127.0.0.1"

这段命令生成一个 2048 位 RSA 密钥和配套的自签名证书,有效期 365 天。-nodes表示私钥不加密,Nginx 启动时不用输密码,方便自动化部署;-subj指定证书主体,其中 CN 字段会被一些旧客户端读取;-addext是生成 SAN 的推荐做法,把访问域名、localhost 和回环地址都写进去。生成后把wss.key和wss.crt放到服务器固定目录,比如/etc/nginx/certs/,后续 Nginx 配置里直接引用这两个文件。

2.3 免费证书的申请与续期:域名验证和服务器落地

生产环境我倾向于用免费 CA 项目签发的证书,这类证书有效期短,但完全受信任,配置思路和自签名没有本质区别。申请流程一般是:在服务器上安装 ACME 客户端,执行签发命令,客户端会要求你证明对域名的控制权,常见做法是通过 HTTP 方式在站点根目录放一个验证文件,或者通过 DNS 解析添加 TXT 记录;验证通过后,证书和私钥会被下载到指定目录,生成fullchain.pem和privkey.pem两个文件。

证书续期是另一个日常动作。免费 CA 证书一般只有 90 天有效期,我习惯用系统定时任务每周跑一次续期脚本,脚本只做两件事:检查证书是否临期、临期则重新签发并重载 Nginx。注意重载要用nginx -s reload,而不是 restart,reload 不会断开已经建立的 wss 长连接,restart 会瞬时把所有在线连接全部踢下线,这在实时业务里会造成大规模闪断。证书文件落位后,记得确认 Nginx 进程对证书目录有读权限,否则握手阶段会直接报权限错误。

3. Nginx 反向代理:把 WS 协议改写为 WSS 的完整配置

3.1 为什么选 Nginx 做 wss 入口而不是在后端直接挂证书

wss 访问的本质是 TLS 握手加 WebSocket 升级握手一起完成,这条路有两种常见走法。一种是后端服务直接加载证书,监听 8443 端口,客户端直连 wss://域名:8443,证书、加密全在后端处理;另一种是 Nginx 对外监听 443,完成 TLS 握手后,把明文 WebSocket 流量转发给后端的内网端口,也就是反向代理模式。我推荐后者,原因有三个:证书续期只需要动 Nginx 配置目录里的文件,后端零感知;443 端口同时承载 HTTPS 页面和 WSS 连接,不需要给用户暴露额外端口;后续如果要加负载均衡或多节点部署,Nginx 这层天然是统一入口。

这里有一个容易混淆的点:Nginx 转发给后端的是 ws:// 还是 wss://?理解了这个,后面的配置就通了。Nginx 负责对外终止 TLS,它和后端之间的连接是内网明文 ws,所以proxy_pass写成http://协议打头的地址就行,不需要在后端再走一遍 TLS。如果后端已经自己挂了证书,Nginx 这边就要把proxy_pass指向后端的 HTTPS 端口,这属于双重加密配置,链路更长,参数更多,一般没必要。

3.2 最小可用配置:443 端口、location 与 Upgrade 请求头

Nginx 转发 WebSocket 和转发普通 HTTP 请求最大的区别在于升级机制。WebSocket 握手靠的是 HTTP/1.1 的Upgrade和Connection两个请求头,Nginx 默认不会把这两个头透传给后端,必须显式配置。经典的 map 配置把空值以外的Upgrade头都映射为upgrade,空值时用close,这样普通 HTTP 请求不受影响:

map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location /wss { proxy_pass http://127.0.0.1:8443; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 120s; proxy_buffering off; } }

这段配置是 wss 反向代理的最小可用模板。location /wss决定了客户端要连接的路径,比如wss://chat.example.com/wss;proxy_pass指向后端 WebSocket 服务地址,这里是本机 8443 端口;proxy_http_version 1.1必须写成 1.1,HTTP/1.0 不支持 Upgrade 机制,漏掉这条,服务端会直接拒绝升级;proxy_set_header Upgrade和Connection把升级请求头原样传给后端,缺一个握手都会失败;proxy_read_timeout是长连接的空闲超时,默认 60 秒对 WebSocket 太短,拉到 120 秒可以配合客户端心跳;proxy_buffering off让 Nginx 不缓冲后端返回的数据,降低大帧消息的延迟。

3.3 参数调优:路径匹配、日志与握手失败时的定位手段

配置写完之后,先别急着连客户端,在服务器上做两个检查。第一是nginx -t验证语法,第二是看 Nginx 的错误日志,路径通常在/var/log/nginx/error.log。wss 握手失败时,错误日志里有几类典型记录:upstream prematurely closed connection多数时候是后端根本没监听那个端口,或者后端服务崩了;invalid upgrade header说明 location 配置正确但 Upgrade 头没传过去,回去检查 map 那段有没有写错位置。

路径匹配是翻车高发地。location 是前缀匹配,/wss会同时匹配/wss和/wss/anything;如果前端访问路径不带末尾斜杠,服务端返回 301 跳转到带斜杠的地址,而 WebSocket 客户端在握手阶段不会跟随 301,连接直接失败。这种问题表面上是 Nginx 配置问题,本质是 location 与前端约定的 URL 不一致。我常用的做法是把路径收敛成单一固定路径,前端和后端约好就用/wss,不做一层以上的路径嵌套,匹配规则简单了,排错自然快。

4. 后端与客户端的 WSS 握手细节:Node.js 示例与连接认证

4.1 后端两种部署模式与 Node.js 的 WSS 实现

后端这边先要确认自己属于哪种模式。走 Nginx 反代时,后端收到的已经是明文 WebSocket 请求,代码里不需要加载证书,监听普通 HTTP 端口即可;如果后端要直接对外提供 WSS,那就要用一个支持 TLS 的 HTTPS 服务承载 WebSocket 服务端。这里用 Node.js 的 ws 库举一个直接承载 WSS 的例子,适合后端独立对外、或做本地全链路验证时使用:

const https = require('https'); const fs = require('fs'); const { WebSocketServer } = require('ws'); const server = https.createServer({ key: fs.readFileSync('/etc/nginx/certs/server.key'), cert: fs.readFileSync('/etc/nginx/certs/server.crt') }); const wss = new WebSocketServer({ server }); wss.on('connection', (ws, req) => { console.log('client connected from', req.socket.remoteAddress); ws.on('message', (data, isBinary) => { ws.send(data, { binary: isBinary }); }); ws.on('close', () => console.log('client closed')); ws.on('error', (err) => console.error('ws error:', err.message)); }); server.listen(8443, () => { console.log('wss server listening on 8443'); });

这段代码先用 HTTPS 服务包了一层 WebSocketServer,key和cert指向证书文件;连接建立后,message事件里把收到的帧原样回发给客户端,方便验证消息链路;error事件必须挂,否则客户端异常断开时,服务端会打印未处理的异常,严重时直接退出进程。注意这里的8443端口只是示例,如果前面还有 Nginx 代理,这个端口应当只监听内网地址,避免 WSS 和明文端口同时暴露在公网。

4.2 客户端连接 WSS:URL 拼接、子协议与二进制帧

客户端这边,最常见的错误是 URL 拼错。wss 协议本身不携带端口信息,前端拿到的地址要么是wss://chat.example.com/wss,要么是wss://chat.example.com:8443/wss,前者走 443 默认端口。拼 URL 时要注意路径必须和 Nginx 的 location 完全一致,大小写敏感,末尾斜杠也会影响匹配结果。下面是一个浏览器端的标准连接写法:

const token = encodeURIComponent(localStorage.getItem('token')); const ws = new WebSocket(`wss://chat.example.com/wss?token=${token}`); ws.addEventListener('open', () => { console.log('wss connected'); ws.send(JSON.stringify({ type: 'ping' })); }); ws.addEventListener('message', (event) => { const text = typeof event.data === 'string' ? event.data : 'binary frame received'; console.log('recv:', text); }); ws.addEventListener('close', (event) => { console.log('wss closed', event.code, event.reason); });

这里的核心点是 token 放到了查询参数里。WebSocket 握手是 HTTP 升级而成,所以查询参数天然可用,后端可以在握手阶段解析并做鉴权,比等数据帧来了再判断更干净。addEventListener的写法比直接赋值回调更安全,多个回调不会互相覆盖。再强调一个容易忽略的差异:message事件里event.data可能是字符串也可能是 Blob,文本协议用JSON.stringify和JSON.parse处理,二进制帧要用event.data.arrayBuffer()再转字节数组,两种类型不要混用,否则粘包解析会非常痛苦。

4.3 连接认证与心跳保活:让 WSS 长连接稳定存活

WSS 和 WS 在握手之后的协议语义完全一样,唯一的不同是前者多了一层 TLS。这意味着之前所有 WS 踩过的坑,WSS 一个都不会少,最常见的就是连接被中间设备掐断。服务器和客户端之间如果没有数据流动,运营商 NAT 映射和 Nginx 的proxy_read_timeout都会到点清理连接,表现就是客户端还显示在线,但消息已经发不过去了。通行做法是心跳保活,客户端每 30 到 60 秒发一个 ping 帧,服务端回一个 pong 帧,或者直接在应用层约定一个 JSON 心跳消息,超过两个周期没收到回应就主动重连。

let alive = true; const heartbeat = setInterval(() => { if (!alive) { console.warn('heartbeat timeout, reconnecting...'); ws.close(); return; } alive = false; ws.send(JSON.stringify({ type: 'ping' })); }, 30000); ws.addEventListener('pong', () => { alive = true; });

这段逻辑用 30 秒定时器做探测:每轮先把alive置为 false,然后发 ping;收到 pong 后再置为 true。如果下一轮开始时alive还是 false,说明上一个 pong 没回来,连接已死,直接关闭并触发重连。这里有个细节,ws.close()之后要记得clearInterval(heartbeat),否则重连后会有多个定时器同时跑,消息会变得极其混乱。生产环境中建议把重连逻辑和心跳定时器放进同一个连接管理对象里,避免状态互相污染。

5. 配置 WSS 访问的避坑记录:五条常见报错与定位路径

5.1 踩坑记录:握手阶段返回 301,浏览器报 unexpected server response

现象:浏览器控制台报Error during WebSocket handshake: Unexpected server response: 301,后端服务日志里没有任何连接记录。原因是请求打到 Nginx 后,被 HTTP 层重定向逻辑拦截了。最常见的是配置里有一个全局的 HTTP 跳 HTTPS 规则,把所有 80 端口请求 301 到 443,这一跳对普通页面没问题,但 WebSocket 客户端不跟随重定向,握手直接失败。解决方法是把 wss 请求路径排除在跳转规则之外,或者让前端直接写对 wss:// 地址,不经过 http:// 入口;另一种原因是 location 前缀匹配不到位,/wss实际命中了别的规则。定位手段是curl -I看返回头,能看到Location字段指向哪里,重定向来源一目了然。

5.2 踩坑记录:证书链不完整,安卓端报 CERT_UNTRUSTED

现象:桌面浏览器访问正常,安卓原生客户端连接 WSS 时抛证书信任异常,iOS 设备部分版本也会拒绝。原因是服务端只下发了叶子证书,没把中间证书一起发过去;桌面浏览器通常自动补齐证书链,移动端的 TLS 栈更严格,缺了中间证书直接不认。解决方法是把证书与中间证书拼接成一个 fullchain 文件,Nginx 里用ssl_certificate /etc/nginx/certs/fullchain.pem指向拼接后的文件。验证手段是openssl s_client -connect 你的域名:443 -servername 你的域名 -showcerts,输出里应该能看到至少两段BEGIN CERTIFICATE,如果只有一段,基本就是链不完整。

5.3 踩坑记录:小消息正常,大消息被截断或后端报文解析失败

现象:心跳、文本小消息收发正常,一旦服务端推送超过 8KB 或 16KB 的数据帧,客户端收到的帧不完整,或后端直接报错。原因是 Nginx 默认开启了缓冲,proxy_buffering off没配置时,代理会先把后端数据攒进缓冲区再转发,而 WebSocket 帧是流式的,缓冲区有限时大帧会被切碎甚至丢弃。解决方法是把proxy_buffering off与必要的proxy_buffer_size配好,同时检查后端 WebSocket 库的maxPayload参数,这是单独限制,默认值往往只有 100KB,超过会被静默断开。这个坑的隐蔽之处在于不是必现,只有业务消息达到一定体积才触发,建议上线前按最大消息尺寸压测一轮。

5.4 踩坑记录:用 IP 地址访问 WSS,证书域名校验失败

现象:通过 wss:// 加服务器 IP 直连,浏览器报证书域名不匹配,换域名访问又正常。原因很直接,证书签的是域名,不是 IP,TLS 握手时客户端拿访问地址去匹配 SAN,IP 不在列表里自然失败。解决方法是生产环境统一用域名访问 WSS,不要在连接地址里写 IP;局域网调试时,把 IP 加进自签名证书的 SAN,用-addext "subjectAltName=IP:1.2.3.4"重新签发。另一个相关问题是内网 DNS 解析不到外网域名,导致明明配了域名却连不上,这时候在本机 hosts 里加一条解析记录即可,不要反过来改代码去连 IP,那是饮鸩止渴。

5.5 踩坑记录:混合内容拦截与跨域 Origin 校验导致连接被拒

现象:HTTPS 页面里,控制台提示 Mixed Content 或 blocked,wss 连接没有发出去;或者连接能建立,但服务端主动断开并带 Origin 相关错误日志。原因是浏览器安全策略禁止 HTTPS 页面发起不安全的 WebSocket 连接,这是混合内容拦截,wss 才能过这一关;而后端对握手请求里的 Origin 头做了同源校验,来源域名不在白名单里就会拒绝升级。解决方法是前端把地址统一改为 wss,后端在握手回调里按白名单校验 Origin,不要图省事直接关掉校验;排查时先看浏览器 Network 面板确认请求是否发出,再看服务端日志里的sec-websocket-origin头,两段都正常再怀疑协议层问题。

6. 让 WSS 配置经得起验收:验证链路和后续收尾习惯

配置完成后,我习惯用三件事做验收,而不是直接拿业务代码试。第一件是用 OpenSSL 手工模拟 TLS 握手,确认证书链和域名匹配没问题:openssl s_client -connect chat.example.com:443 -servername chat.example.com -showcerts,输出里的 subject 和 issuer 字段要符合预期,末尾的Verify return code: 0 (ok)是证书链可信的直接证据。第二件是在浏览器控制台手动构造一个 WebSocket 对象连接 wss 地址,看readyState是否走到 1、握手是否秒开,这一步能绕过业务逻辑单测链路;第三件是打开开发者工具的 Network 面板,过滤 WS 类型,点开连接详情确认 101 Switching Protocols 这个状态码,看到它说明 TLS 和 Upgrade 两步都过去了。

openssl s_client -connect chat.example.com:443 -servername chat.example.com -showcerts 2>/dev/null \ | grep -E "subject=|issuer=|Verify return code"

这条命令在服务器或本机执行都行,直接命中关键信息输出。-servername必须带,否则 SNI 不匹配,拿到的是默认证书而不是你域名那一份。如果Verify return code不是 0,优先查证书文件引用是否正确、fullchain 是否齐全,再去排查系统根证书库有没有过期。

我之前部署某跨平台系统的推送服务时,就是被这个验收习惯救了一回。当时线上已经能收消息,但移动端客户接入后大面积掉线,查来查去发现根因是证书链不完整,桌面浏览器自动补了链,手机端严格校验直接拒连。从那以后我的 WSS 配置流程固定成:证书装好先跑一遍 s_client,确认 Verify 返回 0,再配置 Nginx,最后才联调客户端。这套顺序能挡住绝大多数玄学类握手问题,也省掉了后面无穷无尽的抓包时间。希望这些配置和排错路径能帮到你,把自己手里的 wss 访问真正配稳。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询