前端加密不能替代HTTPS,页面需通过HTTPS加载以确保脚本和密钥安全。构建加密Payload时需按字典序排序字段、序列化后加密,输出转为Base64。使用fetch提交JSON格式的加密数据,避免FormData。密钥需动态导入并严格控制生命周期,私钥不得存入本地存储。
先说一个核心判断:前端加密这事儿,做得再漂亮,也替代不了 HTTPS。本质上,它们是两个不同层面的安全手段。
为啥这么说?如果页面是通过 http:// 加载的,那整个环境对攻击者来说就是透明的。他可以在数据到达浏览器之前,把 Ja vaScript 文件替换掉,把公钥换成自己的,甚至直接拦截你在加密之前输入的明文——加密的逻辑和密钥都暴露在前端,这时候的加密跟没加密一样。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
真正起作用的,是 TLS 那一层的加密。只有当整个页面走的是 https://,浏览器才会信任加载过来的脚本,证书的验证才会生效,SubtleCrypto 的密钥生成流程才不会被降级或者劫持。这里面有几个容易踩的坑:
SubtleCrypto API。http://localhost:3000,那其实只是“前端加密 + 半段 HTTPS”,中间那一截走的还是明文。这种半拉子工程,意义不大。加密不是简单地把字符串拼起来就行,关键是要按照服务端约定的格式序列化后再加密。一个很常见的翻车现场是:先用 JSON.stringify() 把对象转成字符串,然后直接加密,但完全没有处理字段顺序、空值、或者特殊字符的转义问题。结果服务端解密之后,JSON 解析直接挂掉。
建议的做法是:先构造一个标准的对象 → 按照 key 的字典序排序 → JSON.stringify() → 用 UTF-8 编码成 Uint8Array → 最后调用 encrypt()。这中间有几个技术细节需要留意:
importKey() 导入,类型要明确设为 "public",格式根据算法不同,RSA 公钥用 "spki",AES 共享密钥用 "raw"。ArrayBuffer,不能直接塞到表单字段里,得先转成 Base64 字符串(可以用 btoa(String.fromCharCode(...)) 或者 Buffer.from(...).toString('base64'))。null、undefined 这些情况,如果不做处理,后端的解析逻辑大概率会报错。加密之后的 payload 可不能直接当成普通表单字段去提交,因为 enctype="application/x-www-form-urlencoded" 这种编码方式会破坏二进制数据。正确的做法是用 fetch() 手动构造请求体:
method: 'POST',headers 里加上 'Content-Type': 'application/json'。{ "encrypted": "BASE64_STRING", "iv": "BASE64_IV", "timestamp": Date.now() }。FormData 去提交加密数据。因为它默认会用 multipart/form-data 的格式,后端收到的就不是你想要的原始 payload 了。application/x-www-form-urlencoded,那就把加密字段名设为 data,值用 Base64 字符串,同时要确保 URL 编码正确,别忘了用 encodeURIComponent()。Web Crypto 的密钥对象(CryptoKey)是没办法序列化的,也不能跨 iframe 或者 service worker 共享。每次页面加载都得重新导入一次,密钥的生命周期必须严格控制。这个环节有几个容易被忽略的细节:
localStorage 里,哪怕加密了也不行,这违背了密钥隔离的基本原则。/api/public-key 接口拉取一次,配合 ETag 可以避免重复请求。na vigator.credentials 是否可用。某些隐私模式下,SubtleCrypto 会被禁用。