wss://仅保障传输层tls安全,无法防止终端被控或tls终止点后明文泄露,必须在应用层用aes-gcm等方案做端到端加密。

WebSocket 本身不加密消息内容,wss:// 只保障传输层(TLS)安全,一旦终端被控或 TLS 被绕过,明文消息仍可被直接读取。真正在生产环境防窃听、防篡改,必须做应用层端到端加密。
为什么不能只靠 wss://?
很多人上线后只配了 Nginx 的 wss 代理,就以为“已经加密了”,结果审计时发现聊天记录、用户 ID、金额字段全是明文 JSON。这是因为:
-
wss加密的是 TCP 流,解密发生在 Nginx 或服务端 TLS 终止点之后——从 Nginx 到后端 WebSocket 服务(如 Spring Boot、Ratchet、Netty)之间走的是普通ws://或内网 HTTP 协议,数据是裸奔的 - 浏览器内存 dump、调试器 hook、恶意扩展都能在
onmessage回调前/后截获原始消息 - 服务端日志若未脱敏,
console.log(event.data)会把解密后的内容打满日志系统
前端用 Web Crypto API 做 AES-GCM 加密
别再引入 CryptoJS 或硬编码密钥。现代浏览器原生支持 window.crypto.subtle,更安全、无依赖、性能好。重点不是“能不能加”,而是“怎么加才不白加”:
- 密钥必须由服务端动态下发,绑定
session_id或短期access_token,绝不能写死在 JS 里 - 固定使用
AES-GCM:它自带认证标签(tag),解密失败直接丢弃,避免填充预言攻击 - IV 必须每次随机生成且长度为 12 字节(96 bit),和密文一起 base64 传给服务端;不要复用 IV
- 加密前把消息转成
Uint8Array,不要对字符串直接encodeURI—— 中文会乱码 - 附加数据(
additionalData)建议传时间戳 +client_id,服务端解密后校验是否超时/来源合法
示例片段(发送端):
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify({ action: 'transfer', amount: 1000 }));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const key = await importKeyFromServer(); // 从服务端获取的密钥
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, additionalData: encoder.encode(Date.now() + clientId) },
key,
data
);
const msg = {
iv: btoa(String.fromCharCode(...iv)),
data: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
tag: btoa(String.fromCharCode(...new Uint8Array(encrypted).slice(-16)))
};
服务端解密必须严格对齐参数
前后端 AES-GCM 参数错一个字节,就会解密失败。常见翻车点:
- 前端用 12 字节 IV,服务端却按 16 字节解析(比如误用
AES-CBC模式逻辑) - 前端传的
tag是最后 16 字节,服务端没截取、或截多了/少了 - 服务端用
UTF-8解码 base64 后的密文,但前端加密的是原始字节数组(需用Buffer或Uint8Array处理) - 没校验
additionalData:攻击者重放旧消息,只要密钥没换就能通过解密
以 PHP 为例,关键校验代码:
$iv = base64_decode($payload['iv']); $ciphertext = base64_decode($payload['data']); $tag = base64_decode($payload['tag']); $full_ciphertext = $ciphertext . $tag; // 必须指定 tag length,否则 openssl_decrypt 自动忽略认证 $decrypted = openssl_decrypt($full_ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $additionalData);
密钥管理最容易被忽视的三个动作
90% 的前端加密失效,不是算法问题,而是密钥生命周期失控:
- 密钥导入后,立刻清空原始字符串变量:
keyStr = null;别让它留在内存可被console.memory扫描到 - 解密完成的明文数据,不要赋值给全局对象或 Vue data 属性长期驻留;用完即
JSON.parse()后立即丢弃原始ArrayBuffer - 服务端下发密钥时,HTTP 响应头加上
Cache-Control: no-store, no-cache,防止被 CDN 或浏览器缓存
真正难的不是写加解密函数,而是让密钥不落地、不驻留、不复用。每次连接都该有独立密钥上下文,断连即销毁——这点在长连接频繁重连的场景里,最容易被跳过。










