Laravel-WebSockets+Pusher+React SSL连接故障排查求助
问题背景
使用Pusher、Laravel-Echo和laravel-websockets搭建实时服务,HTTP直连服务器IP(端口6001)完全正常,但通过Nginx反向代理的SSL连接(端口6002)出现握手错误,Nginx日志报错:SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::bad key share)。
技术栈
后端依赖
{ "php": "^8.1", "beyondcode/laravel-websockets": "^1.14", "predis/predis": "^2.2", "pusher/pusher-php-server": "^7.2" }
前端依赖
{ "laravel-echo": "^1.15.3", "pusher-js": "^4.4.0", "react": "^18.2.0" }
部署架构
Laravel 10 API后端(内置WebSocket服务)+ React前端,部署在OVH Ubuntu 23.04 VPS,使用Nginx反向代理和Docker容器化部署。
核心现象
- SSL连接触发握手失败,无法建立WebSocket连接
- 服务器IP+6001端口的HTTP连接工作正常
- 服务器端口6001、6002已开放
关键配置详情
后端配置
broadcasting.php
'pusher' => [ 'driver' => 'pusher', 'key' => env('PUSHER_APP_KEY'), 'secret' => env('PUSHER_APP_SECRET'), 'app_id' => env('PUSHER_APP_ID'), 'options' => [ 'host' => env('PUSHER_HOST'), 'port' => env('PUSHER_PORT'), 'cluster' => env('PUSHER_APP_CLUSTER'), 'scheme' => env('PUSHER_SCHEME', 'http'), 'encrypted' => true, 'useTLS' => env('PUSHER_SCHEME') === 'https', ], ],
websockets.php
'apps' => [ [ 'id' => env('PUSHER_APP_ID'), 'name' => env('APP_NAME'), 'key' => env('PUSHER_APP_KEY'), 'secret' => env('PUSHER_APP_SECRET'), 'host' => env('PUSHER_APP_HOST'), 'port' => env('PUSHER_APP_PORT'), 'capacity' => null, 'enable_client_messages' => false, 'enable_statistics' => false, 'enable_logging' => true, ], ],
.env
BROADCAST_DRIVER=pusher PUSHER_APP_ID=XXX PUSHER_APP_KEY=XXX PUSHER_APP_SECRET=XXX PUSHER_APP_CLUSTER=mt1 PUSHER_HOST=127.0.0.1 PUSHER_PORT=6001 PUSHER_SCHEME=http
Nginx WebSocket反向代理配置
server { listen 6002 ssl http2; listen [::]:6002 ssl http2; server_name yourdomain.com; server_tokens off; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS_AES_256_GCM_SHA384:TLS-AES-256-GCM-SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS-CHACHA20-POLY1305-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA; ssl_prefer_server_ciphers on; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; location / { proxy_pass http://127.0.0.1:6001; proxy_read_timeout 60; proxy_connect_timeout 7d; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } access_log /var/log/nginx/access_ws.log; error_log /var/log/nginx/error_ws.log; }
前端Echo初始化代码
const privateChannel = `private-channel.${userID}`; const echo = new Echo({ broadcaster: 'pusher', key: Config.PUSHER_KEY, cluster: Config.PUSHER_CLUSTER, wsHost: Config.PUSHER_HOST, wsPort: Config.PUSHER_PORT, wssPort: Config.PUSHER_PORT, forceTLS: false, encrypted: true, disableStats: true, enabledTransports: ['ws', 'wss'], authEndpoint: Config.PUSHER_AUTH_ENDPOINT, auth: { headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}`, }, }, });
前端配置参数
{ PUSHER_KEY: 'XXX', PUSHER_CLUSTER: 'mt1', PUSHER_HOST: 'yourdomain.com', PUSHER_PORT: 6002, PUSHER_PORT_SSL: 6002, PUSHER_FORCE_TLS: false, PUSHER_AUTH_ENDPOINT: 'https://yourdomain.com/api/broadcasting/auth', }
已尝试无效方案
- 切换Apache反向代理,问题复现
- 将证书添加至Chrome信任列表
- 调整数十种前后端配置组合,无改善
针对性排查与解决步骤
1. 修复Nginx TLS密钥协商配置
bad key share错误通常源于TLS密钥交换算法不兼容,当前Nginx的ssl_ciphers配置过于严格,且TLS 1.3与ssl_prefer_server_ciphers on存在冲突。修改Nginx SSL配置:
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # TLS 1.3无需此配置,避免冲突
修改后重启Nginx:sudo systemctl restart nginx
2. 修正前端Echo配置矛盾
当前前端forceTLS: false与encrypted: true配置冲突,针对SSL连接调整如下:
const echo = new Echo({ broadcaster: 'pusher', key: Config.PUSHER_KEY, cluster: Config.PUSHER_CLUSTER, wsHost: Config.PUSHER_HOST, wssPort: 6002, // 明确指定SSL端口 forceTLS: true, // 强制使用TLS encrypted: true, disableStats: true, enabledTransports: ['wss'], // 仅启用SSL传输 authEndpoint: Config.PUSHER_AUTH_ENDPOINT, auth: { headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}`, }, }, });
3. 验证Docker网络与端口绑定
确保Laravel-WebSockets服务在容器内绑定到所有网卡,修改websockets.php:
'apps' => [ [ // ...其他配置 'host' => '0.0.0.0', // 替换为0.0.0.0,允许容器外部访问 'port' => 6001, // ...其他配置 ], ],
重启Docker容器:docker-compose restart webservice
4. 检查证书有效性
使用openssl验证证书链与握手:
openssl s_client -connect yourdomain.com:6002
确认输出中无证书错误、域名匹配,且握手流程正常。
5. 跳过Nginx代理直接测试SSL(可选)
若上述步骤无效,可让Laravel-WebSockets直接处理SSL:
- 将Let's Encrypt证书挂载到Docker容器中
- 修改broadcasting.php和websockets.php配置:
// broadcasting.php 'options' => [ 'scheme' => 'https', 'certificate' => '/var/www/webservice/certs/fullchain.pem', 'certificate_key' => '/var/www/webservice/certs/privkey.pem', // ...其他配置 ], // websockets.php 'ssl' => [ 'local_cert' => '/var/www/webservice/certs/fullchain.pem', 'local_pk' => '/var/www/webservice/certs/privkey.pem', 'verify_peer' => false, ],
前端直接连接wss://yourdomain.com:6001测试。
内容的提问来源于stack exchange,提问作者Michał Gajak

