React应用通过AMQP over WebSocket连接Azure Service Bus队列遇SASL协商关闭问题
看起来你在把Node.js环境下的Azure Service Bus AMQP over WebSocket连接迁移到React(浏览器环境)时遇到了连接异常关闭的问题,这大概率是因为浏览器环境与Node.js环境的WebSocket处理差异,以及手动SASL协商的实现不符合Azure Service Bus的要求导致的。
先拆解几个关键问题点:
1. URL身份验证方式的误区
你当前把SAS密钥直接放在WebSocket URL中(wss://saskeyname:saskey@sbnamespace.servicebus.windows.net:443/$servicebus/websocket),这种方式在Node.js的nodejs-websocket库中可能被自动转换为SASL PLAIN协议的身份验证帧,但浏览器原生WebSocket并不会自动处理URL中的用户密码信息——服务器收到的只是连接请求,并没有正确的SASL身份验证数据,因此会直接断开连接。
2. SASL协商的实现错误
你的代码中调用了sasl.negotiate(),但这个方法应该是Node.js环境下node-amqp10库的内部方法,在浏览器环境中并没有对应的实现。Azure Service Bus要求使用SASL PLAIN机制进行身份验证,你需要手动构造符合AMQP 1.0规范的SASL帧。
3. AMQP初始握手的缺失
在进行SASL协商前,你需要先发送AMQP的OPEN帧完成初始握手,否则服务器无法识别后续的SASL请求,会直接关闭连接。
修正后的实现步骤与代码示例
步骤1:构造正确的WebSocket连接
首先,URL不需要包含SAS密钥,改为基础的WebSocket端点:
const url = 'wss://sbnamespace.servicebus.windows.net:443/$servicebus/websocket'; const socket = new WebSocket(url, 'AMQPWSB10');
步骤2:实现AMQP握手与SASL PLAIN验证
在onopen事件中,先发送AMQP OPEN帧,再发送SASL INIT和RESPONSE帧:
// 辅助函数:构造AMQP帧(简化版,仅针对SASL PLAIN场景) function createAmqpFrame(type, channel, payload) { const frameSize = payload.length + 8; // 帧头8字节 + 负载长度 const frameBuffer = new ArrayBuffer(frameSize); const view = new DataView(frameBuffer); // 帧头:大小(4字节)、类型(1字节)、通道(2字节)、结束标志(1字节) view.setUint32(0, frameSize, false); // 大端字节序 view.setUint8(4, type); view.setUint16(5, channel, false); view.setUint8(7, 1); // 结束标志 // 复制负载到帧缓冲区 const payloadBytes = new Uint8Array(payload); new Uint8Array(frameBuffer, 8).set(payloadBytes); return frameBuffer; } socket.onmessage = function (event) { console.log('Received frame:', new Uint8Array(event.data)); // 处理服务器返回的帧,比如SASL OK、AMQP OPEN确认等 }; socket.onerror = function (event) { console.error('WebSocket error:', event); }; socket.onclose = function (event) { console.log('WebSocket closed:', `Code: ${event.code}, Reason: ${event.reason}, Clean: ${event.wasClean}`); }; socket.onopen = function (event) { console.log('WebSocket connected:', event); // 1. 发送AMQP OPEN帧(通道0) const openPayload = [ 0x00, 0x00, 0x00, 0x18, // 编码后的OPEN帧大小 0x02, // 类型:AMQP 0x00, 0x00, // 通道0 0x01, // 结束标志 // OPEN帧内容(简化版,符合Azure要求的最小字段) 0x00, 0x00, 0x00, 0x0a, // 帧体大小 0x00, 0x00, 0x00, 0x00, // 容器ID(空) 0x00, 0x00, 0x00, 0x00, // 主机名(空) 0x00, 0x00 // 最大帧大小(0表示默认) ]; socket.send(new Uint8Array(openPayload).buffer); // 2. 发送SASL INIT帧(通道0,类型:SASL) const saslInitPayload = [ 0x00, 0x00, 0x00, 0x20, // 帧大小 0x03, // 类型:SASL 0x00, 0x00, // 通道0 0x01, // 结束标志 // SASL INIT内容 0x00, 0x00, 0x00, 0x16, // 帧体大小 0x00, 0x00, 0x00, 0x05, 'PLAIN'.charCodeAt(0), 'PLAIN'.charCodeAt(1), 'PLAIN'.charCodeAt(2), 'PLAIN'.charCodeAt(3), 'PLAIN'.charCodeAt(4), // 机制:PLAIN 0x00, 0x00, 0x00, 0x00, // 初始响应(空,后续发送RESPONSE) 0x00, 0x00, 0x00, 0x00 // 主机名(空) ]; socket.send(new Uint8Array(saslInitPayload).buffer); // 3. 发送SASL RESPONSE帧(通道0,类型:SASL) // SASL PLAIN格式:\0<sas-keyname>\0<sas-key> const saslResponse = `\0saskeyname\0saskey`; const saslResponseBytes = new TextEncoder().encode(saslResponse); const saslResponsePayload = [ 0x00, 0x00, 0x00, 8 + saslResponseBytes.length, // 帧大小:8字节头 + 负载长度 0x03, // 类型:SASL 0x00, 0x00, // 通道0 0x01, // 结束标志 // SASL RESPONSE内容 0x00, 0x00, 0x00, saslResponseBytes.length // 响应数据长度 ]; // 拼接响应数据 const responseBuffer = new Uint8Array(saslResponsePayload.length + saslResponseBytes.length); responseBuffer.set(saslResponsePayload); responseBuffer.set(saslResponseBytes, saslResponsePayload.length); socket.send(responseBuffer.buffer); };
额外排查要点
CORS配置检查:确认Azure Service Bus允许你的React应用域名访问。虽然日志显示连接打开后关闭,但如果CORS配置不正确,服务器可能在握手后断开连接。你可以在浏览器开发者工具的Network面板中查看WebSocket请求的响应头,确认
Access-Control-Allow-Origin是否包含你的域名。帧格式验证:用浏览器开发者工具的Network面板查看WebSocket的Frames标签,对比Node.js应用中发送的帧(可以通过
node-amqp10的日志输出),确保浏览器发送的帧结构、字节序与Node.js环境一致。SAS密钥编码:如果你的SAS密钥包含特殊字符,需要确保在SASL RESPONSE中使用原始的密钥(不需要URL编码),因为SASL PLAIN是直接传递字节数据。
内容的提问来源于stack exchange,提问作者chii

