在HTML5应用中,WebSocket常用来做低延迟的双向通信,但很多初学者直接通过ws协议发送JSON明文,这在公共网络里极不安全。要保护消息内容,不能只靠传输层,还需要理解浏览器提供的原生加密能力以及如何与WebSocket结合。

一、优先使用wss而非ws协议
WebSocket的加密最基础的一步是启用wss(WebSocket Secure),它本质上是在TLS之上运行WebSocket,类似https之于http。如果前端代码里写的是new WebSocket('ws://ipipp.com/chat'),那么数据在传输过程中没有任何加密,路由器、代理都能直接看到内容。改成wss后,握手阶段和后续帧都被TLS保护。
需要注意的是,wss要求服务端具备合法证书,且页面本身最好也通过https打开,否则浏览器会因为混合内容策略阻止连接。以下代码展示了安全的连接建立方式:
// 使用wss协议,确保传输层加密
const socket = new WebSocket('wss://ipipp.com/chat');
socket.onopen = function() {
console.log('安全通道已建立');
};
socket.onmessage = function(event) {
// event.data在wss下已被TLS保护,但业务层仍可再加密
console.log('收到消息:', event.data);
};
尽管wss解决了链路窃听,但服务端自身、内部日志、或前端内存中的明文仍可能泄露。因此对高敏感字段,还需在应用层再做一次加密,做到双重保护。
二、用Web Crypto API做AES应用层加密
HTML5提供了Web Crypto API,不需要引入任何外部库就能完成标准的AES加密。常见选择是AES-GCM模式,它同时提供保密性和完整性校验。前端在发送前用crypto.subtle.encrypt处理消息,后端用相同密钥和初始向量解密。
下面示例演示如何生成密钥并把一段文本加密后通过WebSocket发送。注意密钥管理很关键,实际项目中不应把密钥写死在前端代码里,而应通过安全通道下发。
// 假设已从安全渠道获得AES-GCM密钥key和随机iv
async function sendEncrypted(socket, key, iv, plainText) {
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const cipherBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
data
);
// 将ArrayBuffer转为base64便于通过WebSocket文本帧传输
const cipherArray = new Uint8Array(cipherBuffer);
let binary = '';
cipherArray.forEach(b => binary += String.fromCharCode(b));
const base64 = btoa(binary);
socket.send(base64);
}
// 接收方解密示例
async function decryptMessage(key, iv, base64) {
const binary = atob(base64);
const cipherArray = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
cipherArray[i] = binary.charCodeAt(i);
}
const plainBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
cipherArray
);
return new TextDecoder().decode(plainBuffer);
}
使用AES-GCM时要保证每次加密的iv都不同,且iv不需要保密但必须不可预测,通常可用crypto.getRandomValues生成12字节随机值。如果iv重复,攻击者可能推算出明文规律,这是很多自写加密代码容易踩的坑。
另外,WebSocket默认以字符串或二进制发送,把加密结果转成base64字符串最简单,也不会有二进制帧兼容问题。若追求更高效率可发送ArrayBuffer,但要确保服务端按相同格式解析。
三、通过RSA协商临时会话密钥
如果前端硬编码对称密钥,一旦代码被扒取密钥就失效。更稳妥的是用非对称加密做密钥交换:服务端生成RSA密钥对,把公钥发给前端;前端用Web Crypto的RSA-OAEP加密一个随机生成的AES密钥,传给服务端,之后双方用该AES密钥通信。
这种方式的优势是每次连接都用不同会话密钥,即使某次被破解也不影响历史消息。下面展示前端导入服务端公钥并加密AES密钥的过程:
// 服务端传来的PEM格式公钥先转为ArrayBuffer再import
async function importPublicKey(pem) {
const b64 = pem.replace(/-----(BEGIN|END) PUBLIC KEY-----/g, '').replace(/s/g, '');
const binary = atob(b64);
const buf = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i);
return crypto.subtle.importKey(
'spki',
buf,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['encrypt']
);
}
async function wrapAesKey(publicKey, aesKey) {
const raw = await crypto.subtle.exportKey('raw', aesKey);
const wrapped = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
raw
);
return wrapped;
}
服务端用私钥解开得到AES密钥后,就可以正常解密前端用该密钥加密的WebSocket消息。整个流程中,私钥永远不离开服务端,前端只接触公钥,安全性比纯对称方案更好。
不过RSA-OAEP运算比AES慢,所以只用来加密很短的密钥数据,业务消息仍走AES。同时要注意浏览器对Web Crypto的限制:该API仅在安全上下文(https或localhost)可用,本地用file协议打开页面会报错。
四、常见错误与排查建议
开发者常犯的一个错误是把加密后的二进制直接当字符串拼接进JSON,导致乱码。应当统一用base64编码再做序列化。另一个问题是iv和salt不随消息发送,服务端无法解密,正确做法是将iv前缀拼在密文前一起传。
还有人试图用废弃的md5或sha1做加密,这是误区,哈希算法只能校验完整性不能保密。下表列出几种方案对比:
| 方案 | 保密性 | 性能 | 适用场景 |
|---|---|---|---|
| 仅wss | 传输层强 | 高 | 普通低敏数据 |
| wss+AES-GCM | 应用层强 | 高 | 聊天、令牌 |
| wss+RSA协商+AES | 前向安全 | 中 | 金融、隐私 |
总结来说,HTML5加密WebSocket消息应先确保wss打底,再用Web Crypto做应用层加密,敏感系统引入RSA密钥协商。这样既能利用浏览器原生能力,也能避开第三方库的维护负担和安全隐患。
HTML5_WebSocket消息加密wss修改时间:2026-08-01 08:42:33