做前端时间长了多少都会碰到 JavaScript 加解密的活儿登录密码不想明文上传、接口参数怕被篡改、敏感字段想在浏览器里先处理一道再提交……这类需求听起来不复杂真的动手写起来坑特别多。我从 CryptoJS 一路用到 Web Crypto API中间踩过字符编码、填充模式、密钥格式、随机数复用一堆坑后来帮团队整理过一整套从前端到网关的加解密约定。这篇东西就是我实操下来沉淀的思路既讲清楚前端加解密的边界也把能直接抄的代码和排查方法给你适合刚开始接触前端加密的新手也适合已经在项目里写了但总觉得不踏实的同学。1. 先搞明白JavaScript 加解密到底解决什么问题1.1 前端加解密的边界能防什么不能防什么先说一句可能得罪人的话很多前端加解密方案其实是自欺欺人。浏览器里跑的 JavaScript代码对用户可见网络请求对抓包工具可见内存里的数据对调试器可见甚至密钥就写在 JS 文件里。所谓“前端加密”在此前提下不可能成为一种真正的安全边界。真正的安全边界在服务端和传输层。TLS 负责网络传输过程不被窃听服务端业务逻辑负责认证和授权。前端加解密在整个链路里扮演的角色是纵深防御中的一环而不是唯一防线。它能干的事归纳起来大概是这么几件减少敏感数据在应用层的明文暴露面、给接口请求加签名防止别有用心的篡改、配合时间戳和随机数降低重放攻击的成功率、满足某些审计场景对字段加密的硬性要求。所以我对团队的要求一直是先分清“前端加密”和“后端加密”。后端加密是安全机制前端加密更像是“提高攻击成本”的辅助手段。如果哪一天你发现自己的加密逻辑已经复杂到影响了开发和排查效率那大概率不是方案不够强而是一开始就把安全边界画错了。任何前端能拿到的东西最终都默认是可以被绕过的这个心态要摆正。1.2 抓包场景明文传输到底有多危险我见过最典型的反面案例是这样的登录页面前端把用户名和密码组装成 JSON一条 POST 请求直接把明文密码带上去了。开发同学说系统已经上了 HTTPS所以加密交给 TLS 就够了这话本身没错但问题在于 HTTPS 只保证传输管道安全管道的两头——浏览器页面、被请求打到的服务端日志——都在裸奔。打开浏览器开发者工具的 Network 面板密码字段清清楚楚地躺在请求体里。如果某个环节在服务端把请求体打到日志文件密码就在日志里永久存档如果前端埋点上报了页面输入事件输入框内容也可能被第三方脚本拿到。这些都是 TLS 管不到的地方。有人会说那我前端先算个 MD5 再传不就安全了吗这里必须说清楚MD5 是哈希算法不是加密算法它是单向的从设计上就不用来保护密码传输。更重要的是普通哈希对常见口令几乎是透明的彩虹表一查就出来了攻击者抓到 MD5 值基本等于抓到密码。退一万步讲就算你换成 SHA-256没有随机盐的哈希同样挡不住字典攻击。所以结论很清楚别指望前端用哈希代替加密也别把哈希和加密混为一谈。1.3 加密不解决所有问题安全靠分层做企业级项目最忌讳把全部希望押在某一个单独方案上。安全这件事从来都是层层叠叠的传输层有 TLS应用层有签名、加密、认证业务层有权限校验、风控策略、操作审计每一层负责挡住一部分攻击剩下的交给下一层兜底。前端加解密的角色就是应用层这一小块。我一般建议按这个优先级排先确保全站 HTTPS 和正确的证书配置再保证服务端对请求做完整校验然后才轮到考虑 JS 里要不要做签名、要不要加密字段。顺序一旦反了就会出现团队花了两周做了一个很精妙的加密方案结果服务端接口连最基本的参数校验都没做攻击者直接把加密后的密文原样回放照样能成功。说到“回放”很多人以为接口带个签名就是安全了实际上签名只解决“参数有没有被篡改”的问题不解决“请求被原样重放”的问题后者需要时间戳、随机数、甚至幂等机制去配合。这些内容在后面章节会展开这里先记住一句话分层防御才是安全前端加解密只是其中的一块拼图。2. 基础原理回顾对称、非对称、哈希与 MAC2.1 对称加密为什么 AES-GCM 是当前首选对称加密的特点是用同一把密钥完成加密和解密速度快、算法成熟适合加密大量数据。当前的主流选择是 AES密钥长度一般用 128 或 256 位。DES、3DES 这类老算法现在不建议再看密钥长度太短计算机算力早就把它们打到不值钱了还在跑这些算法的老系统应当尽快迁移。AES 是分组密码加密时把数据分成固定大小的块然后通过工作模式把块和块之间的关系组织起来。最早见的模式是 ECB它的问题非常直观同样的明文块会生成同样的密文块加密一张图片后轮廓依然清晰可见这在语义上是灾难。CBC 模式引入了初始化向量 IV让同样明文在不同加密里产生不同密文但它只防篡改不防伪造历史上还出现过 padding oracle 这类攻击手法。现在企业项目里我基本只推荐 GCM 模式。GCM 是一种 AEAD 模式加密的同时会生成一个认证标签接收方解密时先验标签标签对不上直接拒绝这样既拿到机密性也拿到完整性一举两得。用 AES-256-GCM配合随机生成的 12 字节 IV再加上可选的附加认证数据 AAD是目前前端能够用到的比较稳妥的组合。后面实战章节我会给出完整代码这里先把“为什么选它”焊死在脑子里。2.2 非对称加密RSA 与 ECC 的取舍非对称加密使用密钥对公钥可以公开分发私钥必须保密。公钥加密的数据只能由私钥解密反之私钥签名、公钥验签。这套机制解决了对称加密里“密钥怎么安全传递”的难题所以 HTTPS 握手、数字证书、混合加密方案里到处都是它的身影。RSA 是其中最普及的算法但它的缺点是性能差、密文长度有限。以 2048 位 RSA 为例它最多只能加密大约 245 字节的数据再多就得自己拆分非常麻烦。在实际项目里直接用 RSA 加密业务数据的情况越来越少更多是用 RSA 加密一把随机生成的 AES 会话密钥再用 AES 去加密真正的业务数据这就是常见的“混合加密”。另外RSA 的填充模式也要留意OAEP 比 PKCS#1 v1.5 更安全新系统优先用 RSA-OAEP。ECC 椭圆曲线算法则是更现代的替代方案密钥更短、计算更快例如 P-256、X25519。它特别适合移动端和低性能设备。日常做 Web 开发不一定需要自己实现 ECC但理解它为什么越来越流行对方案选型有好处——如果你的项目既要考虑前端性能又要考虑密钥长度ECC 通常比 RSA 更省。2.3 哈希、口令哈希与 HMAC不是加密的“加密”哈希算法是单向的数据进去、摘要出来但摘要不可能还原成原文。MD5 和 SHA-1 的碰撞攻击已经成熟不应当再作为安全哈希使用现在的底线是 SHA-256。可很多人的误区在于把哈希当加密用把 SHA-256 当成密码的存储方式。密码存储绝不能直接做普通哈希因为用户口令的熵值低攻击者可以跑字典。标准做法是用专门的慢哈希算法比如 bcrypt、scrypt、argon2它们自带随机盐、计算强度可调跑一次要几十毫秒甚至更久攻击者的成本直线上升。HMAC 则是另一类东西带密钥的哈希。同一个消息没有共享密钥的人就算不出合法的 MAC 值。它的用途是消息认证也就是“确认数据确实来自持有密钥的一方”。打个比方哈希是公开的指纹谁都能计算HMAC 是双方约定好的盖章指纹伪造不了。在接口签名、防篡改这类场景中HMAC-SHA256 是基础。还有一件事必须强调整个加解密体系都建立在随机数之上。IV 要用随机数salt 要用随机数nonce 要用随机数。JavaScript 的 Math.random 绝不能用在这些地方因为它不满足密码学安全随机数的要求。浏览器里要用 crypto.getRandomValuesNode.js 里要用 crypto.randomBytes这是底线没有商量的余地。3. 工具与库选型CryptoJS、Web Crypto API 怎么挑3.1 环境决定第一选择浏览器、Node.js 还是老系统选库之前先回答一个问题代码到底跑在什么环境现代浏览器已经内置 Web Crypto API也就是 window.crypto.subtle提供 AES、RSA、ECDSA、HMAC、SHA 等能力由浏览器原生实现性能和对安全随机数的支持都很可靠。Node.js 从 15 版本开始也支持 globalThis.crypto 的 WebCrypto 接口后端代码可以保持和前端一致的写法。CryptoJS 则是一个纯 JavaScript 实现的加密库老项目里非常常见。它最大的优势是兼容老浏览器、接入简单但性能不如原生实现而且它很多默认行为在跨语言联调时特别容易出问题这点后面细说。如果你的项目没有历史包袱我强烈建议优先用 Web Crypto API如果服务端开发团队语言多样也可以统一约定 Node.js 的 crypto 模块或 WebCrypto 接口争取一套逻辑复用。有个细节要注意Web Crypto API 要求运行在安全上下文里具体来说就是 HTTPS 页面或者 localhost。如果开发时用 http 加 IP 访问或者直接用 file:// 打开会出现“crypto.subtle is undefined”这类报错这不是代码的问题而是环境不支持。遇到这种情况先检查页面协议再查代码。3.2 CryptoJS 的正确打开方式别只抄表面代码CryptoJS 网上教程很多但大多数教程里那种最简单的写法恰恰是跨语言联调的大坑。最典型的是这种代码// 反面示例直接把一个普通字符串当密钥 const encrypted CryptoJS.AES.encrypt(hello, my-secret-key).toString();这句代码看起来漂亮实际上 CryptoJS 内部会用 EVP_BytesToKey拿 MD5 去派生真正的密钥并且会随机生成一段 salt 拼进密文里。结果就是这段密文只能由 CryptoJS 自己解后端用 OpenSSL、Java、Go 的解密代码去解经常对不上。问题的根源是“你以为自己用的是 AES实际用的是一套带自定义 KDF 的封装格式”。正确做法是显式指定密钥、IV、模式和填充让每一份参数都可控。下面是我生产环境里一直在用的写法// 前台加密AES-256-CBC密钥和 IV 都用 Base64 传入 function aesEncrypt(plainText, keyBase64, ivBase64) { const key CryptoJS.enc.Base64.parse(keyBase64); const iv CryptoJS.enc.Base64.parse(ivBase64); const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(CryptoJS.enc.Base64); } // 对应解密 function aesDecrypt(cipherBase64, keyBase64, ivBase64) { const key CryptoJS.enc.Base64.parse(keyBase64); const iv CryptoJS.enc.Base64.parse(ivBase64); const bytes CryptoJS.AES.decrypt(cipherBase64, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return bytes.toString(CryptoJS.enc.Utf8); }这里有个容易踩的细节如果前端传的是普通字符串密钥需要先用 CryptoJS.enc.Utf8.parse 把字符串转成 WordArray再作为 key 传入而不是直接把字符串丢进去。另外同一个错误在不同开发者手里的表现还不一样有的后端用 OpenSSL 解不开有的解出来是乱码总之先统一“密钥用什么编码、IV 放哪里、密文输出什么格式”再去联调。3.3 Web Crypto API 实操AES-GCM 完整代码如果你在做一个新项目我建议直接用 Web Crypto API。它的写法是异步的所有操作返回 Promise而且密钥是 CryptoKey 对象不能用 JSON.stringify 去打印也不太容易被人误存到日志里。这里是一份可以直接跑通 AES-GCM 加密解密的参考代码// 生成 AES-GCM 256 位密钥 async function generateAesGcmKey() { return crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, false, [encrypt, decrypt] ); } // 加密返回由 IV 和密文拼接的结果 async function aesGcmEncrypt(key, plainText, iv, aad) { const encoder new TextEncoder(); const cipherBuffer await crypto.subtle.encrypt( { name: AES-GCM, iv: iv, additionalData: aad }, key, encoder.encode(plainText) ); return bufferToBase64(cipherBuffer); } // 解密 async function aesGcmDecrypt(key, iv, cipherBase64, aad) { const decoder new TextDecoder(); const cipherBytes base64ToUint8Array(cipherBase64); const plainBuffer await crypto.subtle.decrypt( { name: AES-GCM, iv: iv, additionalData: aad }, key, cipherBytes ); return decoder.decode(plainBuffer); } // 工具函数ArrayBuffer 转 Base64 function bufferToBase64(buffer) { const bytes new Uint8Array(buffer); let binary ; for (let i 0; i bytes.length; i) { binary String.fromCharCode(bytes[i]); } return btoa(binary); } // 工具函数Base64 转 Uint8Array function base64ToUint8Array(base64) { const binary atob(base64); const bytes new Uint8Array(binary.length); for (let i 0; i binary.length; i) { bytes[i] binary.charCodeAt(i); } return bytes; }注意这里我对 IV 的处理比较保守没有把 IV 直接拼进密文。实际项目里为了便捷通常把 iv 和密文拼接在一起传输但拼接顺序必须固定、写进文档。GCM 模式本身默认会在加密结果尾部带上认证标签所以解密方只要参数一致标签校验是自动完成的不需要额外操作。3.4 按场景选库RSA、哈希、口令哈希的匹配表下面是我在项目里常用的选型思路罗列出来供你参考。没有一个库是万能的也没必要为了一个 AES 功能引入 500KB 的全家桶。场景推荐工具注意事项浏览器 AES/RSA/HMACWeb Crypto API要求 HTTPS 或 localhostNode.js 服务端加解密node:crypto / webcrypto优先原生实现性能好老项目快速接 RSAjsencrypt默认 PKCS#1 v1.5不支持 OAEP复杂证书、TLS 操作node-forge功能全但体积大按需引入浏览器端口令哈希bcryptjs慢哈希可用 Worker 防卡顿纯摘要计算Web Crypto subtl.digest别再用 MD5/SHA-1实际开发中我见过最痛的问题是项目为了一个 RSA 加密引入一个大而全的库结果和另一个依赖冲突构建体积暴涨。做选型时建议先看自己到底需要哪几种算法再反向决定引什么库能用原生 Web Crypto 解决的绝不额外引包。4. 典型实战场景登录密码、接口签名与密文传输4.1 登录密码到底要不要前端“加密”每次聊到密码处理我都会先给团队吃一颗定心丸只要是标准 HTTPS 环境用户名密码明文提交、服务端用 bcrypt 加盐慢哈希存储这就是全世界最常见的做法也是安全上站得住脚的方案。不要因为谁说了句“前端密码必须加密”就慌着写一堆自定义逻辑反而增加了出漏洞的概率。如果产品确实有降低日志暴露面的需求可以再叠加一层“挑战响应”。流程是这样的用户提交前前端先从服务端接口拿到一个随机 nonce然后用 SHA-256 或者 PBKDF2 对 password 加 nonce 做一次派生计算把派生结果而不是原始密码传上去服务端拿到后用同样的 nonce 重算一遍再校验。这样网络层和日志层都不出现原始密码明文代价是接口多了一次请求、服务端多一次计算。但必须说清楚这个方案不能替代服务端慢哈希存储也不能替代 HTTPS。nonce 对攻击者是可见的它防的不是重放而是“密码明文出现在日志里”。真正的密码安全最终还是要靠服务端在拿到密码后做慢哈希、加盐、限制失败次数这些动作。如果你看到有人在前端用固定 salt 把密码哈希后再提交那只说明这个人没搞懂 salt 的意义——salt 公开没关系但固定的 salt 让彩虹表攻击依旧可行毫无增益。4.2 接口防篡改与防重放HMAC 签名 时间戳 nonce接口签名是我在企业项目里用得最频繁的一项能力。典型场景是 H5 页面调用业务接口时请求参数可能被中间层篡改或者被攻击者抓包后重放。签名的基本思路是把业务参数、时间戳、随机数按既定规则拼接成一个字符串用双方共享的密钥做 HMAC-SHA256得到签名串放到请求头里。服务端拿到请求后把同样参数拼出来重算一次签名比对成功才放行。签名串的构造必须严格统一。我习惯先把参数按 key 排序过滤掉空值然后逐项做 URL 编码最后拼上时间戳和 noncefunction buildSignMessage(params, timestamp, nonce) { const keys Object.keys(params).sort(); const parts keys .filter((key) params[key] ! undefined params[key] ! null) .map((key) ${key}${encodeURIComponent(params[key])}); return ${parts.join()}timestamp${timestamp}nonce${nonce}; } // 使用 Web Crypto 计算 HMAC-SHA256 async function signMessage(message, secretBase64) { const encoder new TextEncoder(); const key await crypto.subtle.importKey( raw, base64ToUint8Array(secretBase64), { name: HMAC, hash: SHA-256 }, false, [sign] ); const signature await crypto.subtle.sign(HMAC, key, encoder.encode(message)); return Array.from(new Uint8Array(signature)) .map((b) b.toString(16).padStart(2, 0)) .join(); }服务端要做三件事。第一校验时间戳比如 |timestamp - now| 超过 5 分钟直接拒绝第二用 Redis 或者数据库记录 nonce同一个 nonce 用两次就不放行第三重算签名并做常量时间比较防止时序攻击。只有这三个条件同时满足请求才被视为合法。为什么必须同时有时间戳和 nonce因为时间戳把重放窗口压缩到了几分钟nonce 保证同一窗口内同一请求不能被第二次使用。这两个缺失任何一个签名方案都会漏。比如只有签名没有时间戳攻击者可以一年后原样重放只有时间戳没有 nonce攻击者可以在窗口内重复重放如果接口是下单、转账一类的操作影响就很明显。4.3 敏感字段加密一次性会话密钥的混合加密方案有些业务场景里特定字段必须做应用层加密比如手机号、身份证号、银行卡号这类要求多来自合规审计。最稳妥的前端方案不是我前端写死一把 AES 密钥然后给后端对暗号而是“后端下发 RSA 公钥 前端生成一次性 AES 密钥”的混合加密。流程是这样后端生成 RSA 密钥对私钥保存在服务端或云 KMS公钥通过 HTTPS 接口下发给前端。前端随机生成一把 AES-GCM 256 位密钥这一把密钥只在当前请求里有效。前端用这把 AES 密钥加密手机号等敏感字段得到密文。前端用后端公钥对这把 AES 密钥做 RSA-OAEP 加密得到密钥密文。前端把 IV、字段密文、密钥密文一起提交给后端。后端先用 RSA 私钥解出 AES 密钥再用 AES 密钥解出业务字段。用这样的方式真正的业务密钥没有在网络上明文传输过每次请求都用新密钥旧的密钥没法解密其他请求的数据私钥始终没有离开服务端。下面是核心代码async function importPublicKey(spkiPem) { const body spkiPem .replace(-----BEGIN PUBLIC KEY-----, ) .replace(-----END PUBLIC KEY-----, ) .replace(/\s/g, ); const der Uint8Array.from(atob(body), (c) c.charCodeAt(0)); return crypto.subtle.importKey( spki, der, { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); } async function encryptSensitiveData(plainText, spkiPem, aadText) { const encoder new TextEncoder(); // 1. 生成一次性 AES-GCM 密钥 const aesKey await crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt] ); // 2. 随机 IV12 字节 const iv crypto.getRandomValues(new Uint8Array(12)); // 3. 用 AES 加密业务字段AAD 可以绑定用户ID、接口路径 const cipherBuffer await crypto.subtle.encrypt( { name: AES-GCM, iv, additionalData: encoder.encode(aadText) }, aesKey, encoder.encode(plainText) ); // 4. 导出 AES 原始密钥用 RSA 公钥包装 const rawAesKey await crypto.subtle.exportKey(raw, aesKey); const publicKey await importPublicKey(spkiPem); const wrappedKey await crypto.subtle.encrypt( { name: RSA-OAEP }, publicKey, rawAesKey ); return { version: 1, iv: bufferToBase64(iv), cipher: bufferToBase64(cipherBuffer), wrappedKey: bufferToBase64(wrappedKey) }; }这里面值得多说一句的是 additionalDataAAD的用法。AAD 是不加密的但在认证范围内任何一位 AAD 变动都会导致认证失败。把用户 ID 或者接口路径放进 AAD相当于把密文和上下文绑定起来攻击者把 A 接口的密文搬到 B 接口的时候认证直接失败这是一个很实用但很多人不知道的细节。4.4 H5 文件处理与多端互通图片、上传与桥接参数H5 页面里做文件加密比如身份证照片、合同附件的安全上传核心思路是把文件读成 ArrayBuffer然后用 AES-GCM 加密再封装成 Blob 上传。文件往往比较大加密性能和内存回收需要提前评估。我建议在 Web Worker 里做加密避免阻塞 UI 线程加密完成之后要用 URL.createObjectURL 生成临时链接给页面预览预览用完立刻 revokeObjectURL否则在 iOS Safari 上内存占用会慢慢爬上去。还有一类场景是 H5 与原生应用互相调用比如 JS 调起原生 SDK或者原生 WebView 向 JS 传数据。这种 Bridge 通信里传的几乎都是字符串加解密产生的二进制数据必须转成 Base64 再传。我见过好几个线上问题都是因为原生端拿到字符串做了截断或替换特殊字符密文到服务端就解不开了。跨端的加解密参数必须在开发第一天就固定下来包括密钥格式、IV 长度、密文编码、签名串拼接顺序最好沉淀成文档让三端都对着一张表开发。移动端的网络环境也比较复杂弱网下请求重试会带来一个隐含问题前端每次重试时如果重新生成随机密钥原始请求和重试请求的密文可能对不上服务端幂等设计时要兼容这一点。我的建议是重试时复用同一份加密参数或者干脆把业务幂等键放到签名串里由服务端做去重。5. 企业级落地密钥管理、防重放与安全评审5.1 密钥管理前端不应该持有“真密钥”业界对密钥管理的原则是密钥分级、最小权限、动态轮换。落到前端场景核心结论就是——不要让前端拿到任何长期有效的业务密钥。前端拿到的应该是短期会话密钥或者只是 RSA/ECC 公钥。即使公钥本身不怕被看见也要通过正规接口分发而不是写死在 JS 包里。因为写死在代码里的公钥一旦需要轮换就要发版动态下发的公钥可以随时换不用等用户升级页面。我见过一个比较务实的做法是在密文前面加版本号。比如上传的密文格式统一为 version keyVersion iv ciphertext服务端看到 version 和 keyVersion 后从自己的密钥库里找到对应版本号的密钥去解密。这样线上可以同时跑两套密钥新请求全部用 v2失败率极低之后再停掉 v1轮换期间不影响用户。这个思路虽然多写几行解析代码但能解决“密钥轮换必须停机”的尴尬。日志里不要打印密钥这是老生常谈但总有人犯。尤其在前端加密代码里顺手 console.log 一个 iv、一个 rawKey页面一跑控制台全裸奔。这类代码评审时必须抓出来。5.2 防重放与幂等前端如何配合服务端做闭环前面讲的签名加时间戳加 nonce属于应用层的防重放常用组合。但如果你面对的是支付、下单这类高价值操作还需要进一步叠加幂等机制。简单说就是前端每次发起操作时生成一个 requestId连同签名一起提交服务端用一个 Redis 的 SETNX 把 requestId 存下来处理成功后返回下次遇到同一个 requestId 直接拒绝或者返回上一次的响应结果。前端生成 requestId 要用 crypto.randomUUID()而不是 Math.random 拼时间戳否则并发情况下极易撞车。浏览器兼容性方面现代浏览器和 Node.js 16 基本都支持老浏览器可以用 crypto.getRandomValues 手搓一个 UUID v4。还有一层是风控。前端配合做设备指纹、操作埋点、触发验证码都是把“异常”信号反馈给服务端。但前端能做的只是传递信号最终判断必须在服务端统一完成前端任何“我觉得没问题就放行”的逻辑都不该存在。5.3 JS 与原生/服务端互通时的编码和版本兼容一个加解密方案最怕的不是算法选错而是各端之间“约定”不统一。有一次项目里前端用 CryptoJS 加密后端用 Go 解密怎么都解不开最后发现前端加密时用了非常规的盐拼接格式Go 侧标准库根本不认。这种问题一般在联调阶段爆炸如果发现得晚会导致上线前返工。跨端协作时至少要定下这些内容算法AES-256-GCM、密钥编码Base64、IV 长度12 字节、密文编码Base64、AAD 内容哪些字段参与、时间戳格式毫秒还是秒、签名字符串排序规则。这些全部写进接口文档并且放到仓库里的共享常量文件中让三端共同引用。老技术栈的兼容也容易踩坑。比如早期一些用 ASP.NET WebForms 写的后端Java 和 .NET 对 RSA 密钥格式的处理不同前端拿到的公钥在 jsencrypt 里能导入到了 Web Crypto 里就得做 SPKI 和 PKCS#1 的格式转换Java 默认用 PKCS1PaddingNode 的 RSA-OAEP 却要明确指定 SHA-256。这类坑不是算法问题是生态差异问题多预留联调时间别在最后一天才开始对接。5.4 上线前的安全自查清单下面这份清单是我在每次涉及加解密的前端改动发布前都会过的你可以直接拿去用检查项通过标准传输层全站 HTTPS无 http 明文接口弱算法无 DES、3DES、MD5、SHA-1 用于安全场景密钥安全前端不硬编码长期密钥私钥只存在服务端/KMS随机数IV、nonce 使用 crypto.getRandomValues无 Math.random日志代码和日志中不出现明文密钥、完整敏感字段服务端校验服务端对签名、密文、nonce 做独立校验不依赖前端测试向量三端共享同一组固定测试向量有回归用例依赖安全npm audit 或等效检查无高危漏洞错误处理解密失败时不暴露堆栈统一返回模糊错误如果你的团队没有专职安全人员建议把这张表贴在需求评审文档里由项目负责人逐项打勾。前端同学容易只盯着自己那一亩三分地觉得“我这边加密了就行”实际上攻击者的路径往往是跨层拼接的只有把整条链路都过一遍方案才算闭环。6. 常见问题与排查技巧实录6.1 编码问题密文对不上盘的罪魁祸首在加解密联调里十个报错里至少有七个是编码问题。同样的二进制数据用来转字符串就有 UTF-8、Base64、Hex、Latin1 一堆选择同样叫 Base64前端脚本里处理换行符的方式和后端还不一样。最崩溃的排查体验是加密端和解密端代码看着都对就是结果对不上这时候不要怀疑算法先检查每一行编码转换。我调试时的固定动作是把每个环节的数据形式打出来明文是字符串、UTF-8 编码后是字节数组、AES 加密后是二进制 Buffer、Base64 输出后是字符串。中间任何一步转错字节序后面就全废了。另外浏览器里的 atob/btoa 对中文和超长内容不友好建议先转成字节数组再操作不要直接拿中文字符串去 btoa。如果遇到crypto.subtle抛 DOMException第一步检查页面是不是 HTTPS第二步检查 key 的用法是否正确比如签名操作是否给了解密用途的密钥这两个原因占了绝大多数。6.2 IV、nonce、AAD 的雷区IV 的雷区永远是“重复使用”。CBC 模式下 IV 重复会让相同明文产生相同密文泄露模式信息GCM 模式下 nonce 重复直接威胁到密钥安全这是密码学上特别严重的问题。所以 IV 必须每次随机生成长度按算法要求来GCM 推荐 12 字节。生成 IV 的代码要放在加密之前一行都不能少。密文传输时的字段拼接顺序也要统一。有人喜欢把 iv 拼在密文前面有人喜欢把 AAD 放后面服务端解析时必须知道确切的位置。我建议在传递结构里用明确的字段名而不是简单地把 iv 和密文拼成一个大字符串这样至少可读性好、不容易分割错。前端解密报“解密失败”时先检查 iv 是否每天都重新生成再检查 AAD 与加密时是否完全一致经常就是差一个字符。GCM 模式下认证标签默认是 128 位Web Crypto API 在这个值是固定的但某些语言库允许配置更短的标签长度。跨端对接时如果发现后端一直提示认证失败建议把标签长度明确写到文档里免得两边理解不一致。6.3 RSA 公钥、填充与密钥格式的兼容问题RSA 前端实践里最典型的坑有两个公钥格式和填充模式。PEM 公钥可能是-----BEGIN PUBLIC KEY-----SPKI 格式也可能是-----BEGIN RSA PUBLIC KEY-----PKCS#1 格式不能用同一套解析逻辑去处理。Web Crypto 的 importKey 默认要求 SPKI而 java 的老接口可能生成 PKCS#1这时前端要先做格式转换或者在服务端统一输出格式。jsencrypt 默认用的是 RSA PKCS#1 v1.5 填充Java 服务端调用RSA/ECB/PKCS1Padding能对上但如果你切换到 Web Crypto 的 RSA-OAEPJava 侧也得同步改成 OAEP 并指定摘要算法。这种不匹配不会报编译错误只会在解密时抛异常排查起来非常耗时间。在浏览器里调试这种问题时我习惯把按钮事件写得干净一点。有些老页面喜欢给链接写javascript:void(0)占位点击后什么也不做这在排查加解密报错时特别容易误导人——你会怀疑是不是 JS 没执行、事件没绑定其实代码根本没跑。建议调试时用显式的事件监听并在回调里打日志报错定位快很多生产环境不要依赖这种伪链接做交互。6.4 测试向量最容易被忽略的回归保障加解密代码最怕“升级之后就坏了”。CryptoJS 同一个版本在不同浏览器上行为有差异Node 升级大版本后底层 OpenSSL 也可能变化如果没有回归用例你可能压根不知道它们变了。所以我强烈建议每个涉及加解密的项目都维护一份独立于实现的测试向量 JSON 文件里面写死一组明文、密钥、IV、AAD、预期密文和预期签名前端、后端、App 共同引用同一份文件。{ algorithm: AES-256-GCM, keyBase64: 9Kj3sE..., ivBase64: 5y4dGt..., aad: userId1001path/api/order, plainText: 13800138000, cipherBase64: 7Gp2vK..., authTagBase64: 4HnqF9... }前端测试可以很简单用 Jest 或者 Node 断言跑一遍“加密后解密是否回到原文”和“固定输入是否得到固定输出”。后端也做同样的单测。只要测试向量能对得上版本升级就不会引起恐慌对不上说明某个环境的行为变了这时候还来得及查比线上解不开好得多。6.5 常见错误速查表表现常见原因处理思路解密出来是乱码明文编码不一致常见中文 UTF-8 问题加密前统一 TextEncoder/UTF-8后端解不开 CryptoJS 密文CryptoJS 默认 KDF 和盐格式非常规显式传 key/iv不使用默认封装浏览器报 crypto.subtle undefined页面非 HTTPS 或 file:// 环境切换到 HTTPS/localhostRSA 解密失败填充模式不一致统一 PKCS1 或 OAEP并固定摘要same IV 导致密文相同IV 复用或写死crypto.getRandomValues 每次生成接口重放成功无时间戳或 nonce 校验加窗口校验和 nonce 去重大文件加密卡顿主线程阻塞放到 Web Worker 中加密最后说一个我自己的固定习惯。每接一个涉及加解密的项目我都会在仓库里放一份 test-vectors.json把算法、模式、密钥、IV、AAD、明文、密文、签名串全部固定下来前端、后端、App 三端都引用同一份测试向量。有一次升级核心加密库就是因为这份文件提前逮住了 CryptoJS 和服务端 OpenSSL 对 salt 处理不一致的问题硬生生把一次线上事故变成了测试阶段的一行失败日志。加解密这件事技术本身并不神秘真正决定项目上线后稳不稳的往往是那些写进文档、写进测试用例的“约定”。如果你正在设计新项目我建议先把 HTTPS、服务端校验和审计日志做好再考虑要不要在 JS 里加一层应用层加密——不要为了加密而加密更不要在你也不清楚它到底防什么的时候把一个自己都解释不清的方案推上线。
企业数字化 ERP 产品动态
相关推荐
理解ax调度:从OFDMA到TWT,路由器多设备优化指南 先说我最近被问爆的一个场景:路由器是新买的 Wi-Fi 6(802.11ax)旗舰、宽带千兆、手机也支持 Wi-Fi 6,可一到晚上全家上网,视频会议照样卡、游戏照样跳延迟。很多人第一反应是换路由器、调天线、抱怨运营商,… · 2026/9/26 4:46:58
基于SpringBoot+Vue的高校学生饮食推荐系统前后端分离实战 做了这套前后端分离的高校学生饮食推荐系统,前后折腾了小两个月,从搭骨架到部署上线踩了不少坑,今天把它完整记录下来。项目本身用的是SpringBootVueMyBatisMySQL这套非常经典的组合,功能覆盖了用户登录、菜品推荐、饮食管理、评论… · 2026/9/26 4:46:52
从AI助手到Agent操作系统:WorkBuddy落地实践与工程解析 如果你最近在刷技术社区,应该能明显感觉到一个风向:AI 工具圈的词库迭代速度,比电脑系统更新还快。前两年大家还在聊“哪个AI助手更聪明”,到了今年,关键词已经变成了 Agent、Skill、MCP、工作台。WorkBuddy 就是在这个… · 2026/9/26 4:46:52
AI辅助论文数据分析:从研究设计到结果呈现的完整工作流 你写论文的时候,最拖时间的是哪一步?我自己的答案一直很稳定:不是读文献,不是做实验,也不是调格式,而是数据分析。拿到一堆原始数据以后,哪怕只是画一张像样的描述统计表,背后都得翻… · 2026/9/26 6:31:15
AI与影视融合实战:2026年从剧本到成片的AI辅助流程与工具选型 1. AI与影视融合的底层逻辑与行业背景1.1 为什么2026年成了融合的分水岭我在影视后期和AI工具链这个交叉领域摸爬滚打了几年,2026年开年这两个月给我的感受非常直接:AI不再是影视行业里那个“锦上添花的小工具”,而是开始往制片流程的骨头缝里… · 2026/9/26 6:31:15
不可变对象如何彻底解决并发线程安全问题 1. 为什么共享对象总在并发里翻车1.1 线程安全到底在防什么先聊个最常见的场景:多个线程同时读一个 HashMap,或者同时操作同一个 SimpleDateFormat,线上动不动就出现脏数据、死循环、甚至 CPU 拉满。你去排查,发现代码写得没什么问… · 2026/9/26 6:31:09
为什么光伏退役总在最后一步返工:退役资产处置流程里容易漏掉的几个节点 以为是卖废铁,结果在交接单上卡了壳
很多人以为光伏电站退役或者工厂处理积压料,无非就是叫几辆大卡车,把屋顶和仓库里的东西一股脑装车拉走。现实往往会在交接签字的前五分钟,狠狠给人上一课。
最常见的尴尬场景是这样的… · 2026/9/26 6:31:09
8个可视化Playbook:lavish-axi如何教会Agent写出看得懂的HTML制品 8个可视化Playbook:lavish-axi如何教会Agent写出看得懂的HTML制品 【免费下载链接】lavish-axi HTML is the new markdown. Lavish is the new editor for your HTML artifacts. 项目地址: https://gitcode.com/gh_mirrors/la/lavish-axi
lavish-axi… · 2026/9/26 6:31:09
棉花病害检测数据集实战:YOLO标注格式解析与训练调优 简介:这份棉花植物病害图像目标检测数据集面向从事农业视觉检测、YOLO 系列模型训练与改进的开发者及学生,提供约 4,600 张已标注图像及对应标签,覆盖枯萎病、卷曲、灰霉、健康、叶斑病等 6 个类别,可直接用于目标检测模型的训练、… · 2026/9/26 6:31:02
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46