简介面向Spring Boot开发者的WebSocket安全配置示例包完整演示在Spring Boot 2.1中启用wss访问的改造过程。压缩包共66个文件、61KB以Java源码、XML配置和Properties配置为主另含JKS证书、Maven构建脚本及Git版本目录等结构精简便于查阅。资源提供项目级的WebSocket处理器、SSL证书加载方式、前端JavaScript连接示例及依赖组织思路可直接对照迁移到已有HTTPS环境。已有13111人学习适合需要为实时通信接口加入加密安全层的中高级后端工程师。1. 先解决一个不常见的需求给WebSocket加一把TLS的锁webSocket配置wss访问这东西在本地调试时几乎没人碰。你开个ws://localhost:8080/ws浏览器连得飞快后端日志刷刷打。可真到了上线那天页面一换httpsWebSocket就再也连不上了——浏览器控制台直接给你红字Mixed Content。这不是后端逻辑错了是浏览器根本不让你在https页面里发起ws请求。要么把整站退回http要么给WebSocket通讯也加上TLS。前者属于开倒车后者就是我们今天要做的wss配置。正文读完你会发现wss不是玄学它就是把WebSocket握手协议放进TLS隧道里走和HTTPS共用443端口握手完再升级成WebSocket帧。这件事在SpringBoot里做起来并不复杂难点全在证书格式、代理层转发和浏览器安全策略上。这篇笔记适合正在做SpringBoot WebSocket改造、被wss连接不上折磨过的后端工程师也适合那些想搞明白ws和wss到底差在哪、却被一堆博客绕晕的新手。2. wss到底改了什么从ws到wss证书与握手的底层逻辑2.1 ws和wss的差别不止是端口多了个443先说结论ws和wss在协议层的关系和http与https几乎一模一样。ws走明文wss走TLS加密后的通道。URL前缀从ws://变成wss://默认端口从80变成443。很多人在这一步就停下来以为换个协议头就行结果接二连三翻车——因为wss不只是加密传输它还改变了握手的几个关键行为。正常情况下ws握手是明文发送HTTP Upgrade请求服务器响应101状态码后直接升级。wss握手时底层先做一次TLS握手完成证书验证和密钥协商然后在TLS隧道内再走HTTP Upgrade。对WebSocket协议本身来说握手包的内容几乎没有变化但承载它的通道从裸TCP变成了TLS加密流。这意味着三件事。第一服务器必须有合法证书。自签名证书在浏览器里会有警告而在WebSocket场景下警告通常不具备继续访问的点击项——你会发现根本连不上。第二证书的域名和访问地址必须匹配IP直连、localhost、域名混用会引发证书校验失败。第三如果是自签证书用于测试客户端必须显式信任该证书Java后端做客户端时还要往truststore里导入这一步忘了就是SSLHandshakeException。我一般把wss改造成本拆成三块证书准备、后端配置、代理层透传。很多人卡在第三块——后端明明配好了外部访问还是失败因为反向代理把Upgrade头给吃了。2.2 SpringBoot下注册WebSocketHandler与注册器的分工SpringBoot里WebSocket的标准做法是一个消息处理类加一个配置注册类。消息处理类继承TextWebSocketHandler或BinaryWebSocketHandler重写afterConnectionEstablished、handleTextMessage、handleTransportError这些方法。配置注册类实现WebSocketConfigurer接口在registerWebSocketHandlers方法里把路径和Handler绑起来。核心代码框架如下Component public class DemoWebSocketHandler extends TextWebSocketHandler { Override public void afterConnectionEstablished(WebSocketSession session) { // 连接建立后记录session或广播在线状态 } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 收到消息后处理可解析JSON、转发给其他session等 String payload message.getPayload(); session.sendMessage(new TextMessage(ack: payload)); } Override public void handleTransportError(WebSocketSession session, Throwable exception) { // 连接异常断开时清理资源注意空指针 } }注册配置类Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { private final DemoWebSocketHandler demoHandler; public WebSocketConfig(DemoWebSocketHandler demoHandler) { this.demoHandler demoHandler; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(demoHandler, /ws/demo) .setAllowedOrigins(*); } }注意setAllowedOrigins它控制的是同源策略。开发阶段用*省事但生产环境如果配合了鉴权建议改成具体的域名列表。ws和wss在跨域处理上是一致的不允许跨域时浏览器握手阶段就会收到403。注册路径要注意协议版本兼容。有的旧浏览器只支持Hixie-76协议SpringBoot的WebSocket是RFC 6455标准手机端和旧浏览器的适配问题不在配置层面解决一般靠SockJS。如果不需要兼容老旧环境别加withSockJS()否则连接路径和握手逻辑都会多一些额外判断。2.3 证书选型和私钥格式PEM、JKS、PKCS12怎么选这是wss配置里最让人头大的环节。证书来源不同拿到的文件格式也不同。常见的有从云厂商下载的Nginx格式PEM证书加PEM私钥、从Java生成的JKS文件、从第三方CA获取的PFX/P12文件。SpringBoot里推荐用PKCS12格式也就是.p12或.pfx文件。原因是PKCS12是标准格式能被Java自带的keytool直接管理还能同时承载证书链和私钥。JKS是Java专有格式虽然也能用但keytool还在持续支持新项目我更倾向直接用PKCS12。如果手里拿的是PEM证书公钥和私钥文件比如fullchain.pem和privkey.pem就需要转成PKCS12统一格式。转换命令在openssl客户端里完成注意私钥没加密的情况下命令是基础操作如果私钥带了passphrase命令里要加参数。参考转换命令openssl pkcs12 -export \ -in fullchain.pem \ -inkey privkey.pem \ -out server.p12 \ -name tomcat \ -passout pass:yourStrongPassword-in指证书链文件-inkey是私钥文件-name是别名-passout给导出的p12设置密码。这个密码后面要填进SpringBoot配置里千万别在代码里写死应放在配置文件或环境变量里统一管理。2.4 三个必须显式配置的SpringBoot参数端口、证书、密码SpringBoot内置Tomcat开启HTTPS配置集中在application.yml的server节点下。关键参数是server.port、server.ssl.enabled、server.ssl.key-store、server.ssl.key-store-type、server.ssl.key-store-password、server.ssl.key-alias。配置示例server: port: 8443 ssl: enabled: true key-store: classpath:server.p12 key-store-type: PKCS12 key-store-password: yourStrongPassword key-alias: tomcatclasspath:server.p12表示证书放在src/main/resources目录下。密码和证书不要提交到Git仓库我习惯用${SSL_KEY_STORE_PASSWORD}占位符部署时从环境变量读入。这里有个容易忽略的点端口用8443是为了本地测试方便不占用443。但生产环境不会让SpringBoot直接监听443的往往前面还有Nginx或云负载均衡。真正常用的端口是443对外由代理层把wss请求转发给SpringBoot的8443端口。于是问题就变成了代理层怎么把wss原封不动转发过去。3. SpringBoot开启wss从证书导入到完整配置3.1 第一步把证书转成PKCS12格式的命令上一章已经给了openssl转换命令这里补充实战细节。证书文件常见问题有两个一是全链证书里泥土杂质的换行符或多余空格导致解析失败二是私钥和证书不匹配导致启动直接抛异常。转换前建议先做一些快速检查openssl x509 -in fullchain.pem -noout -subject -dates openssl rsa -in privkey.pem -noout -modulus第一条命令看证书主题和有效期第二条命令看私钥的modulus。如果证书和私钥匹配度没问题转出来的p12一般都能正常使用。检查时还可以对比一下两边modulus是否一致不一致就是文件不配套。3.2 第二步application.yml里写全ssl配置这一步配置在2.4节已经给了基础版本这里补上两个进阶配置项。一个是server.ssl.client-auth默认是NONE表示不需要客户端出示证书。如果后端要求双向TLS设为NEED但业务场景里很少用到。另一个是server.ssl.enabled-protocols指定TLS协议版本。出于安全考虑建议配置为TLSv1.2,TLSv1.3把TLSv1和TLSv1.1排除掉。实测经验是部分低版本安卓WebView在TLSv1.2下正常旧设备可能出问题但2025年了该放弃就放弃。3.3 第三步WebSocketConfigurer注册时端点路径与allowedOrigins的坑这一步坑很隐蔽。setAllowedOrigins传的是完整origin但很多人直接写https://mydomain.com本地联调时又换成http://localhost:8080来回改配置非常痛苦。我的做法是把它做成配置文件项Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(demoHandler, /ws/demo) .setAllowedOrigins(allowedOrigins); }allowedOrigins从配置里注入。开发环境设成*或localhost生产环境设成正式域名。注意setAllowedOrigins内部的校验是前缀匹配https://mydomain.com不会自动匹配https://mydomain.com:8443如果前后端端口不同要明确写完整地址。还有一个反向问题如果setAllowedOrigins不加默认不允许跨域请求。本地调试页面端口和后端端口不一致时握手会被拒报403。这个错特别像配置问题其实是跨域策略没放开。3.4 第四步客户端连接的URL写法后端配置完成后前端连接地址的拼写容易写错。常见写法是const ws new WebSocket(wss://${window.location.host}/ws/demo);用window.location.host动态获取当前域名和端口避免硬编码。注意如果页面本身就是HTTPS协议头必须写wss://而不是ws://否则浏览器会继续报Mixed Content错误。另外WebSocket URL里的路径要和后端注册的路径完全一致包括大小写和斜杠。/ws/demo和/WS/demo是两个地址后端Handler不会匹配后者。4. 代理层转发才是wss的翻车高发区nginx与网关配置要点4.1 为什么SpringBoot配好了外网还是连不上最常见的一个现象后端启动无报错openssl测试端口也通浏览器却白屏转圈。打开DevTools看到WebSocket请求状态码是200或者直接Failed。如果后端日志里压根没有握手请求进来那几乎可以确定是代理层拦截了。WebSocket握手依赖两个关键HeaderUpgrade: websocket和Connection: Upgrade。普通的HTTP反向代理默认不会透传这两个头只会把连接当成普通HTTP请求处理返回一段静态响应或者在TCP层直接断开。Nginx从1.13版起对WebSocket提供了官方样板支持。关键是必须显式设置Upgrade头、Connection头以及HTTP/1.1协议版本。注意proxy_read_timeout也要调大否则空闲连接会被默认的60秒断开——很多连得上但一会儿就掉线的问题就出在这里。4.2 Nginx的proxy_pass与Upgrade头少了这两行就断连Nginx配置片段如下server { listen 443 ssl; server_name mydomain.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; location /ws/ { proxy_pass http://127.0.0.1:8443; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header 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 3600s; proxy_send_timeout 3600s; } }proxy_set_header Connection upgrade是核心少了这一行Nginx会把请求当作普通HTTP转发后端无法完成101状态码升级。proxy_http_version 1.1也必须带上因为HTTP/1.0不支持Upgrade机制。location路径的匹配规则容易被忽略。上面配置里location /ws/和SpringBoot的/ws/demo是匹配关系。如果你的WebSocket端点注册在/ws/demoNginx的location写/ws/没问题如果注册的是/demo就需要改成对应的location前缀。WebSocket握手URL里的路径决定了匹配哪个location然后Nginx把请求原样转发给代理目标。路径拼接时不会有额外的路径重写proxy_pass http://127.0.0.1:8443;后面没有尾斜杠表示不重写URI。如果写成proxy_pass http://127.0.0.1:8443/;URI的/ws/demo会变成/demo立即404。这是典型的路由翻车案例。4.3 X-Forwarded-Proto与forwarded头让后端知道原始协议后端如果存在重定向逻辑或根据协议生成绝对URL就依赖这些转发头来判断最初是HTTP还是HTTPS。wss场景下后端收到的其实是HTTP请求代理层用HTTP转发给Tomcat如果没有X-Forwarded-Proto后端会认为请求是明文进来的。SpringBoot 2.2以上内置Tomcat提供了forwarded-header-strategy配置项。推荐设置server: forward-headers-strategy: framework这样SpringBoot会自动识别X-Forwarded-Proto、X-Forwarded-For等标准头。如果不加这个配置某些重定向场景下会生成http开头的地址哪怕整个链路都是https。注意如果同时启用了Spring Security的redirect策略转发头配置错误会让OAuth2登录跳转死循环这类问题排查起来极其隐蔽。另一点是不要把X-Forwarded-Proto硬编码为https如果代理链路上还有一层HTTP负载均衡器硬编码会掩盖真实协议。5. 避坑与常见问题wss配置里最容易踩的五个坑5.1 握手失败浏览器报WebSocket connection failed但后端日志无记录现象浏览器DevTools里看到请求状态码是404或403后端完全没收到消息。 原因请求路径没匹配到代理层或后端Handler或者location与proxy_pass的URI拼接规则把路径改了。 解决先看浏览器请求的实际URL确认wss://mydomain.com/ws/demo完整可访问。再检查Nginx location是/ws/还是/以及proxy_pass是否带了尾斜杠。最后在SpringBoot里加个日志打印request.getRequestURI()比对实际进来的路径。5.2 自签名证书导致连接直接被拒现象本地测试用自签证书浏览器显示证书无效且没有继续访问按钮。 原因WebSocket握手阶段的TLS证书校验失败浏览器不给绕过机会。 解决测试环境让浏览器信任自签证书或者用前端的代理插件方式但根因解法是申请正式CA证书。开发环境如果只是本地联调可以反过来把SpringBoot的ssl先关掉用ws调试联调通过再开启wss。5.3 偶发性断开一分钟后连接自动掉线现象连接稳定建立但空闲一会儿后自动断开重连也无效。 原因代理层设置了较短的proxy_read_timeout或者负载均衡默认空闲超时时间很短。 解决将Nginx的proxy_read_timeout和proxy_send_timeout调大通用做法是3600s。如果前面还有云负载均衡器需检查其连接空闲超时设置常见的云LB默认60秒就断TCP连接必须在LB或后端加心跳。心跳机制是业务层控制通常是前端每隔30秒发一个ping帧后端回pong。5.4 Java客户端连wss报SSLHandshakeException现象后端Java服务作为客户端去连另一个wss服务端报PKIX path building failed。 原因目标服务端证书未被本机JRE的cacerts信任。 解决将证书导入到客户端的truststorekeytool -import -alias mycert -keystore cacerts -file server.crt注意cacerts默认密码是changeit导入生产环境前要想好密钥库管理策略。然后将Java启动参数指向自定义truststore-Djavax.net.ssl.trustStore/path/to/truststore.jks。这比直接改JRE默认cacerts更可控。5.5 代理层SSL终止与后端SSL重复现象Nginx配了443证书后端8443也配了证书结果连接建立失败或性能暴跌。 原因做了两层SSL代理层和SpringBoot各加密一次客户端证书校验隔了一层自签情况下尤其容易出错。 解决最常见拓扑是SSL termination架构——代理层终结HTTPS到后端走明文HTTP。Nginx转发目标直接写http://127.0.0.1:8080后端不开启ssl。如果你必须保留后端SSL比如安全合规要求全链路加密那要保证客户端信任的是最终服务端的证书链代理透传TCP流量而不是重新握手。6. 验证与进阶除了浏览器控制台还有三招可以确认wss真的通了浏览器控制台看到Network里的websocket请求101就万事大吉了吗实测中101只能说明握手成功不代表消息能双向跑通。我常用三招来确认链路完整性。第一招用openssl命令行直接探测TLS握手openssl s_client -connect mydomain.com:443 -servername mydomain.com如果证书链正常输出会包含Verify return code: 0 (ok)和协商后的加密套件。这一步能排除证书链不完整、域名不匹配等隐藏在浏览器包装背后的错误。如果Verify return code非零浏览器里大概率也是连不上的只是错误信息被吞掉了。第二招用wscat做一次完整的WebSocket连通性测试。wscat需要Node环境全局安装它能用wss地址发起连接主动发消息并等服务端响应——这比浏览器刷新页面更细粒度地定位消息通路问题。wscat -c wss://mydomain.com/ws/demo连上后输入任意文本如果服务端有回包逻辑就会立刻返回。注意wscat不校验证书链的自签信任自签环境下它能连上浏览器连不上这能帮你区分是证书信任问题还是协议握手问题。第三招也是最常被忽略的检查生产环境的远端访问而不是本机。很多人在开发机上curl、wscat都通过了一到客户环境还是失败。区别往往出在中间还有一个企业防火墙或流量审计设备。我一般会让客户环境的技术人员从内网发起openssl s_client确认TCP 443到目标可达再做wscat测试。这三步都过了wss才算真正的通。进阶技巧同一个443端口同时承载HTTPS页面和wss服务。做法是让Nginx按location区分转发——静态页面走普通proxy_pass/ws/路径走Upgrade转发规则。这样前端只需要用相对协议头//mydomain.com/ws/demo页面是HTTPS时自然升级为wss不用在代码里三元判断协议。把wss配置这件事做完后我每次上线前都强制走一遍流程先在本机用openssl检查证书链再通过wscat连一次最后让运维从外网打一个包确认TCP可达。这套习惯帮我擋掉了很多次明明配好了却连不上的上线事故。希望这篇笔记也能帮你少走同样的弯路。本文还有配套的精品资源点击获取