资讯动态

前端加密实战:AES、RSA、MD5算法原理与踩坑指南

发布时间:2026/10/18 16:31:29 来源:尧图企业网站定制
做前端时间久了一定会遇到一个绕不开的话题登录密码要不要在提交前加密接口参数能不能防止被篡改文件下载要不要校验完整性这些问题背后其实都是同一个知识栈——AES对称加密、RSA非对称加密、MD5哈希算法。很多同学一听到这三个名词就觉得头皮发麻觉得这是后端或者安全工程师的活儿跟日常写页面没什么关系。但实际上只要你接手过登录接口、支付回调、数据上报或者任何一个稍微正经一点的业务系统你都会在某个版本迭代里被突袭式地问一句“这个字段为什么是明文”这篇文章会把三个算法的原理、前端落地姿势、代码示例和踩坑实录一次性讲清楚尽量用大白话解释把每次加密选型背后的“为什么”也拆开。看完以后你至少能做到知道什么场景该用AES、什么场景该用RSA、什么场景MD5只是一个辅助工具并且能在浏览器和Node环境里亲手把代码跑起来。适合刚接触前端加密的新手也适合被后端抛来一段加密SDK但完全看不懂的资深页面仔。1. 三种算法的角色定位先搞清楚谁在干什么1.1 对称加密与非对称加密的关系像保险柜和钥匙匣AES属于对称加密特点是加密和解密用同一把密钥。你可以把AES想象成一把能锁能开的普通挂锁锁门和开门用的是同一把钥匙。它的优势非常明显——速度快、硬件支持好、加密大数据量时开销小。拿真实数据说话一台普通电脑上跑AES-256-GCM每秒加密几百MB的数据量毫无压力这速度是RSA完全追不上的。RSA属于非对称加密特点是加密解密的钥匙不一样分为公钥和私钥。公钥随便发给别人负责加密私钥只留在自己手里负责解密。这里我用一个更贴切的类比公钥是任何人都可以往里投信的邮箱口私钥是只有你能打开的那个邮箱锁。别人把信塞进去哪怕经过很多双手也没人能在不砸邮箱的情况下把信拿出来。RSA的问题也一眼可见算法复杂、速度很慢加密几百字节的小数据还行真要加密几MB的数据会跑得让人怀疑人生。所以常规做法是把两者结合起来用AES加密大数据用RSA加密那个AES密钥。也就是前端随机生成一个AES密钥然后用后端下发的RSA公钥把这个AES密钥加密上传给后端后端用RSA私钥解出AES密钥再用这把AES密钥去解业务密文。这个模式叫混合加密是HTTPS背后TLS握手的核心逻辑也是很多自定义安全协议的标配。理解了这一层再看任何一套加密方案你脑子里会有一个框架底层业务数据用对称加密密钥传递用非对称加密。1.2 MD5是哈希算法不是加密算法这层窗户纸必须捅破MD5和AES、RSA最大的区别在于它不是可逆的。MD5把任意长度的数据映射成一个固定长度128位的十六进制字符串理论上你无法从这串字符串反推出原始数据。所以严格来说MD5不是加密加密至少要求能解密哈希是单向的、不可逆的。它是为了“校验完整性”和“生成指纹”而生的。日常开发里最常见的误解就是“密码用MD5加密一下存数据库”。这句话在安全的视角下等于没说因为MD5只做散列不加盐、不迭代、无密钥根本扛不住彩虹表和碰撞攻击。正确的密码存储姿势是bcrypt、scrypt、argon2这类专用密码哈希算法它们故意把计算过程设计得很慢让暴力破解的成本成倍上升。所以MD5在前端更适合干这些事文件校验、缓存key、短链指纹、防篡改签名。它承担的是“证明数据没被改过”的职责而不是“让数据不被人看见”的职责。想明白这个角色区别你就能避免很多低级设计失误。加密解决的是机密性问题哈希解决的是完整性问题签名解决的是真实性问题。三者协同工作才是完整的安全链路光靠某一个算法扛不起所有需求。2. AES对称加密在前端的落地实操2.1 算法模式怎么选别碰ECB优先GCMAES本身是个原始算法使用时有多种工作模式常见的有ECB、CBC、GCM、CCM。ECB模式有一个非常经典的问题相同的明文块会被加密成完全相同的密文块从密文上能直接看出明文的规律性比如图片加密后还能看到轮廓这等于没加密。而CBC模式虽然引入了IV初始向量解决了ECB的规律性问题但它不提供完整性校验——密文被篡改后解密端根本发现不了需要额外叠加MAC或者HMAC才能保证不被篡改。GCM模式是目前最推荐的选择。它天然内置了认证标签authTag不仅加密还能校验数据有没有被篡改。也就是说GCM同时解决机密性和完整性两个问题一石二鸟。在前端环境和后端服务对接时只要后端支持GCM优先选它准没错。密钥长度方面128位在理论上已经具备足够安全性但工程上大家更习惯直接用256位多出的那一点点性能开销几乎可以忽略却可以名正言顺地在验收报告里写上AES-256。这里有个细节GCM加密的IV推荐12字节96位这是一个和底层实现高度相关的约定前后端必须完全一致否则解不开。2.2 浏览器端用Web Crypto API跑通AES-GCM现代浏览器都原生支持Web Crypto API不需要引入额外的加密库。用它做AES-GCM加解密比我以前用第三方库时要干净得多也更难踩到库版本差异的坑。下面是浏览器端加密一段文本的完整示例// 生成一个256位的AES密钥 const key await crypto.subtle.generateKey( { name: AES-GCM, length: 256, }, true, [encrypt, decrypt] ); // 加密函数输入原文输出iv 密文 authTag的Base64组合 async function encryptWithAesGcm(plainText, aesKey) { const encoder new TextEncoder(); const data encoder.encode(plainText); const iv crypto.getRandomValues(new Uint8Array(12)); // 12字节随机IV const encrypted await crypto.subtle.encrypt( { name: AES-GCM, iv: iv, }, aesKey, data ); // encrypted 是 ArrayBuffer密文后面自动跟着16字节的authTag const ciphertext new Uint8Array(encrypted); const combined new Uint8Array(iv.length ciphertext.length); combined.set(iv, 0); combined.set(ciphertext, iv.length); return bufferToBase64(combined); } function bufferToBase64(buffer) { const bytes new Uint8Array(buffer); let binary ; bytes.forEach((b) (binary String.fromCharCode(b))); return window.btoa(binary); }这个代码有三个关键点需要重点说。第一IV每次加密都必须重新生成绝对不能固定。如果IV固定相同明文会产生相同密文等于泄露了明文规律GCM的安全性基础就崩塌了。第二crypto.subtle.encrypt返回的密文末尾已经自动附带了16字节的GCM认证标签前端不需要单独处理标签直接把iv和密文拼在一起发给后端即可。第三加密后的数据是二进制格式不能直接放进JSON里传输必须先转成Base64或十六进制字符串我这里选择了Base64传输体积增加约33%。解密端同样用Web Crypto API完成逻辑是反过来的从Base64里拆出前12字节作为IV剩下的部分作为密文加认证标签然后调用crypto.subtle.decrypt。如果通信过程中有任何一位数据被篡改decrypt会直接抛出一个OperationError这就是GCM自带的完整性校验在起作用。这个报错是好事说明设计真的在拦截恶意修改。2.3 Node.js侧如何解密前端传来的AES密文如果前端工作在浏览器而后端是Node.js服务经常会遇到两边加密库不同导致的对不齐问题。Node端用内置的crypto模块就能完成解密不需要额外安装任何包。关键点在于前端把iv和密文拼在一起发过来后端必须按同样的顺序拆分并且使用同样的加密算法、同样的密钥、同样的字节长度。const crypto require(crypto); function decryptAesGcm(combinedBase64, keyBuffer) { const combined Buffer.from(combinedBase64, base64); const iv combined.subarray(0, 12); const authTag combined.subarray(combined.length - 16); const ciphertext combined.subarray(12, combined.length - 16); const decipher crypto.createDecipheriv(aes-256-gcm, keyBuffer, iv); decipher.setAuthTag(authTag); let decrypted decipher.update(ciphertext, null, utf8); decrypted decipher.final(utf8); return decrypted; }这个示例里我单独拆出了authTag原因是Node的createDecipheriv不会像浏览器那样自动识别附加在密文尾部的标签需要手工切出来并通过setAuthTag喂给它。前端可能会觉得浏览器端计算比较方便但为了跨端一致建议团队内固定下来同一种报文组织方式比如统一规定12字节IV 密文 16字节authTagBase64编码后传输。这个约定如果前后端各写各的后面联调时一定会踩到“解密出来是乱码”的坑。2.4 AES实操里的四个常见坑第一个坑是密钥格式。前端生成的AES密钥默认是CryptoKey对象不能直接当成字符串发给后端需要先用crypto.subtle.exportKey(raw, key)导出成ArrayBuffer再转成Base64传给后端。反过来后端传过来的密钥字符串前端要通过crypto.subtle.importKey(raw, base64ToBuffer, { name: AES-GCM }, true, [encrypt])导成CryptoKey才能使用。这两个方向搞反任何一个都会在importKey环节就报DataError。第二个坑是编码不一致。前端把密文转成了Base64后端却按hex去解析结果自然是乱码前端拼数据时用了Uint8Array组合后端在拆数据时却用Buffer.concat拼接两者底层字节顺序也可能不一致。解决方案没有捷径只能靠严格约定通信格式并且把格式检查写进接口文档。第三个坑是IV复用。有人为了图方便把IV写死成固定值这在GCM模式里属于严重的安全事故。GCM的安全性依赖每次加密使用不同随机IV一旦IV复用同一密钥下不同密文之间的内部关系就可能被破译者利用等于白加密了。所以务必确保每次调用加密函数都重新生成IV。第四个坑是“密钥写在前端代码里”。说实话前端代码里的任何静态密钥都等于公开密钥用户打开开发者工具就能看到。AES密钥必须由后端动态下发或者在每次会话中随机生成并通过RSA信封传递绝不能写死在config.js里。遇到这种需求我一般直接回怼需求方这不是加密这是自欺欺人。3. RSA非对称加密的正确姿势3.1 密钥长度和填充方式这两个参数别乱定RSA的安全级别建立在密钥长度和大数分解的难度之上。目前业内共识是至少2048位一些安全性要求更高的系统会直接上3072位或4096位。长度增加会直接影响加密、解密速度和密钥体积但前端通常只做公钥加密性能压力没那么大所以建议默认2048位起步有钱有性能再考虑更长的。填充方式也是在联调时最容易翻车的地方。常见有PKCS#1 v1.5和OAEP两种。我在自己的项目里一律推荐RSA-OAEP它在Web Crypto API中的名称是RSA-OAEP配合SHA-256摘要算法使用比PKCS#1 v1.5更抗攻击。很多后端同学默认配置是PKCS#1 v1.5如果前端这边用OAEP解两边一碰必然报错。最好的做法是在接口文档里明确标注前端使用RSA-OAEP后端对应算法是RSA/ECB/OAEPWithSHA-256AndMGF1Padding。看到这种参数别慌它们算的是同一件事只是Java、OpenSSL、Web Crypto三种生态的叫法不一样。还需要知道一个致命限制RSA加密的数据长度有上限。以2048位密钥配合OAEP填充为例一次最多能加密约190字节左右的明文256字节减去2*64字节哈希和填充开销。所以RSA天生不适合加密大字段更不能直接加密整个请求体。这也是为什么必须走混合加密RSA只负责加密那一把随机的AES密钥AES负责加密真正的业务JSON。3.2 前端用RSA公钥加密的完整流程常见场景是后端生成密钥对把公钥以Base64编码的字符串方式下发到前端前端拿到公钥后再去做加密。公钥通常采用PKCS#8或者SPKI格式前端不能直接用字符串调用encrypt必须先解析成ArrayBuffer再走importKey导入成CryptoKey。下面是一个可以直接用的封装函数async function importRsaPublicKey(base64PublicKey) { const binaryDer base64ToBuffer(base64PublicKey); return crypto.subtle.importKey( spki, binaryDer, { name: RSA-OAEP, hash: SHA-256, }, true, [encrypt] ); } async function encryptWithRsa(publicKeyBase64, plainText) { const publicKey await importRsaPublicKey(publicKeyBase64); const data new TextEncoder().encode(plainText); const encrypted await crypto.subtle.encrypt( { name: RSA-OAEP, }, publicKey, data ); return bufferToBase64(encrypted); }这段代码背后的坑主要集中在base64ToBuffer这个函数上。公钥Base64字符串往往带有-----BEGIN PUBLIC KEY-----这类PEM头传进来之前需要把头和换行符剥掉只保留中间那一段密文内容。如果后端下发的是十六进制字符串前端就得换一种解析方式。我建议把公钥格式统一为不带PEM头的纯Base64由后端做格式处理前端只负责导入和加密双方责任边界会清晰很多。3.3 私钥为什么绝对不能出现在前端提到RSA就必然会有人问能不能前端用私钥签名后端用公钥验签实际上把私钥放在前端等同于把保险柜钥匙挂在保险柜门上。前端代码是运行在用户浏览器里的任何静态资源都可能被调试、抓包、篡改私钥一旦被提取整个公钥体系就形同虚设。前端需要“签名”的场景正确做法应该是把签名放到服务端完成前端只负责提交待签名的数据如果一定要在客户端做身份认证优先考虑WebAuthn、硬件密钥或者平台自带的生物识别能力而不是把私钥写进JS包。我见过某些项目为了省事直接在前端预置一个固定RSA私钥用来做请求签名结果运营同学用抓包工具分分钟就把私钥还原了出来。这个教训是非对称加密的安全性建立在私钥保密的前提下一旦私钥公开任何加密都只是表演。前端的RSA职责只保留在“用公钥加密数据”解密和签名都留给后端。3.4 混合加密的报文结构长什么样前端随机生成一个256位AES密钥用RSA公钥加密AES密钥本身同时用AES-GCM加密请求JSON最后把一个完整的报文发给后端。报文结构大致是{ encryptedKey: Base64(RSA加密后的AES密钥), encryptedData: Base64(IV AES密文 authTag) }后端收到后先用RSA私钥解密encryptedKey拿到AES密钥再用这个AES密钥和报文里的IV去解密encryptedData。这种混合方案既绕开了RSA加密长度限制又避免了AES密钥在网络上明文传输几乎是目前自定义加密协议的最优解。相比直接使用HTTPS它能额外保护接口层的数据比如在网关、日志系统里不会暴露原始字段同时又能让系统在复杂中间件环境下多一层纵深防御。需要强调的是前端加密不能替代HTTPS它和HTTPS不是二选一的关系而是在正确使用HTTPS的前提下做进一步增强。4. MD5哈希算法安全边界和实用技巧4.1 MD5能用在哪些地方先说结论MD5不能用于密码存储也没有密钥参与所以它不是“加密”技术。但MD5在工程里依然有很多不可替代的用途它的特点是快、短、固定长度适合生成指纹。前端可以利用MD5做下载文件的完整性校验拿到文件后计算MD5和服务器下发的MD5比对一致就说明文件没有被篡改或传输损坏。缓存体系也经常拿它做key比如把一长串URL参数通过MD5生成一个短ID避免缓存键过长。另一个常见场景是接口防篡改签名。前端把请求参数按字典序排序拼上双方约定好的盐值secret整体计算MD5把这个值放在请求头里。后端收到请求后按同样的规则重新计算比对两份MD5是否一致不一致就直接拒绝。这个方案的优点是实现简单不依赖复杂的密钥协商缺点是MD5本身可以被碰撞攻击所以它只适合中等安全等级的业务不适合高价值交易场景。如果需要更强安全性的签名可以把MD5换掉改成HMAC-SHA256这种带密钥的哈希算法抵抗碰撞的能力会更强。4.2 浏览器和Node里分别怎么算MD5浏览器端的Web Crypto API不提供MD5算法支持原因很简单MD5已经被认为不够安全浏览器厂商不愿意再为它提供原生接口。所以在浏览器里要算MD5通常借助第三方库比如crypto-js。注意使用前要确认一下库的整体体积和依赖情况如果项目本身很小只为了算一个MD5引入整个crypto-js可以考虑用纯JS实现或者改用非对称签名的方案。const CryptoJS require(crypto-js); const params { mobile: 13800138000, amount: 99.00, timestamp: 1710000000, }; const secret 你的约定盐值; const signStr Object.keys(params) .sort() .map((key) ${key}${params[key]}) .join() .concat(key, secret); const sign CryptoJS.MD5(signStr).toString();Node.js环境则简单得多内置的crypto模块直接支持MD5const crypto require(crypto); function md5(content) { return crypto.createHash(md5).update(content).digest(hex); }这里有两个小细节值得注意。一是拼签名串时团队内部必须约定好“是否包含值为空的参数”“数组参数如何序列化”“嵌套对象的key排序规则”否则前后端各写各的排序签名永远对不上。二是crypto.createHash的update方法可以链式调用如果内容是流式读取一定要确保所有字节都写进去了再调用digest漏掉任何一段数据MD5结果都是错的。4.3 签名能防篡改但防不了重放MD5签名解决的问题是“数据有没有被改过”但它不关心请求是不是原来的请求。攻击者完全可以截获一个合法请求原封不动地重放多次后端如果每次都校验通过就会造成重复下单、重复扣款之类的严重后果。这是我在实际项目中踩过的坑只做了签名校验没做防重放结果线上出现了一笔订单被重复提交的案例。标准解法是加上时间戳和随机数nonce。前端在参数里带上当前客户端时间戳和一次性随机字符串后端校验签名通过后再检查时间戳是否在允许的时间窗口内比如±5分钟并且检查这个nonce是否在有效期内的缓存中出现过。如果nonce已经出现过说明这是一次重放请求直接拒绝。这样设计后重放窗口被压缩到几秒钟以内风险就基本可控了。需要提醒的是时间戳窗口和nonce缓存这些逻辑必须由后端来完成前端只是被动带上这两个参数不要指望前端做拦截因为攻击者完全可以绕过前端。5. 前端加密工程的选型和取舍思路5.1 先想清楚加密目标再选算法我刚工作那会儿遇到需求就条件反射地堆技术参数加密、返回值加密、头部签名恨不得把整个请求包装成密文。后来发现这种过度设计不仅性能差还给前后端联调制造了大量麻烦。加密方案的设计应该从威胁模型出发系统到底要防谁是防外部的抓包和重放还是防内部的日志泄露还是防用户篡改客户端数据不同的威胁模型对应不同的方案盲目堆料只会增加复杂度。比如说只是防第三方监听那HTTPS已经足够前端再加密一遍属于重复投入如果系统里存在网关日志、审计平台会记录完整报文那敏感字段级加密就有必要如果担心用户直接修改请求参数刷接口那签名字段就能解决问题如果担心被重放攻击那就组合时间戳和nonce。技术选型要多问几句“为什么”而不是网上看到什么方案就抄什么方案。5.2 密钥应该动态下发而不是写死在包里AES密钥写在前端是公开的RSA公钥公开是没问题的但私钥绝对不能出现。对于自定义加密协议我强烈建议每次接入或每次会话都动态生成密钥。前端在会话开始时请求一个/api/security/public-key接口后端返回RSA公钥前端随机生成AES密钥用公钥加密后作为握手请求的第一个字段发送后端用私钥解出AES密钥之后的整个会话都用这把AES密钥加密通信。这个流程可以和上面说的混合加密报文统一起来每次会话密钥都是新的即使某一次通信被破解也不会影响后续会话的安全性这种特性在安全领域叫前向保密。实现起来并没有想象中那么复杂无非是给现有前端请求层增加一个“获取公钥”和“加密请求体”的通用拦截器。但收益非常明显不用再担心静态密钥泄露、密钥更新困难的问题。如果团队里没有专门的安全工程师这个方案是我们能做出来的最有性价比的折中。5.3 性能优化和包体体积控制加密不是没有代价的。RSA加密2048位密钥数据时耗时通常在几十毫秒到百毫秒之间虽然一次请求加密很小但如果是批量加密几百个字段延迟会非常明显。AES-GCM的速度则快到几乎可以忽略加解密几十KB数据也就是微秒级到毫秒级。因此在设计接口时能不加密大数据就不加密大数据能合并加密的字段就合并成一个JSON对象整体加密减少调用次数。还有一个容易被忽略的细节压缩和加密的顺序。可压缩的数据必须先压缩再加密因为加密后的数据近乎随机压缩算法几乎找不到冗余信息压缩率会急剧下降等于白压。 HTTP传输层如果开启了HTTPS和可能的压缩协商也要注意不要对已经加密的报文再做压缩效率极低且浪费CPU。密文本身经过Base64编码后体积增加约三分之一如果服务端和客户端都支持二进制传输可以考虑直接用ArrayBuffer而不是Base64能省一些流量。5.4 前端加密不是银弹别忽略全局风险需要清醒认识到前端加密的本质是提高了攻击门槛而不是彻底阻断攻击。攻击者完全可以不破解你的加密协议直接篡改前端代码把加密函数替换成明文函数或者干脆不发你的页面自己构造请求。所以任何依赖前端来保证安全的设计都必须同时具备后端的二次校验和风控逻辑。业务上的关键规则比如价格、库存、优惠券状态后端必须重新校验不能轻信前端传来的任何值。前端加密保护的是传输过程和数据落盘后的泄露风险而不是业务逻辑的正确性。在落地前可以先画一张简单的数据流向图把每个环节的敏感字段标出来页面渲染时、请求发送时、网关记录时、服务端接收时、数据库存储时。哪一段是明文哪一段是密文一目了然。前端加密能管到的只是“请求发送时”和“部分存储前准备”这两段其他环节的安全需要整个团队配合完成。这也是我一直强调的原因加密是系统工程不是写几个函数就完事。6. 常见问题与排查技巧速查表把这个表格贴在这里是我根据自己的排查经验整理的。某种意义上这个表比正文里的代码更实用因为遇到报错时查问题原因才是常态。现象可能原因排查思路和解决办法前端报了DataError导入密钥失败公钥格式不对或者Base64没解码干净确认去掉PEM头尾、换行符用atob前检查字符串长度是否为4的倍数让后端换用统一的纯Base64编码后端解密AES密文报tag mismatchGCM的认证标签没有正确切分或密文被截断确认拆包规则IV长度12字节authTag最后16字节检查传输过程是否发生了编码转换比如号丢失后端能解密但内容是乱码加密算法一致但编码不一致统一密文编码格式要么都Base64要么都hex确认TextEncoder和Buffer之间没有默认编码不一致问题MD5签名后后端总是验签失败参数排序不一致或拼接规则不一致统一按ASCII字典序排序空字段是否忽略、数组如何序列化都要在文档写清楚建议后端导出验签代码片段供前端对照RSA加密大字段报错明文长度超过密钥最大加密长度限制改用混合加密用AES加密大数据体用RSA加密AES密钥或者把字段拆分后分段加密不推荐分段复杂度高前端无HTTPS环境下Web Crypto API报错非安全上下文不支持crypto.subtle生产环境必须用HTTPS本地开发用localhost可以内网IP访问时部分浏览器也会限制同一密钥加密相同明文得到相同密文IV固定或者IV生成逻辑有问题每次加密都要重新生成随机IV检查是否有全局缓存导致IV复用签名通过但接口被重复调用没有防重放机制增加时间戳nonce字段后端对nonce做幂等校验签名通过后再判断重复最后再分享一个我自己的实操习惯前端加密代码的调试不要等接口联调时才做。写一个小的纯函数测试页把“加密 - 模拟后端解密 - 对比结果”在这个页面上跑通可以大幅缩短后续联调的时间。测试页里我会预置几组固定的密钥和IV专门用来验证编解码函数业务代码里再单独抽一个加密服务模块所有加密逻辑集中管理。用这种方式我过去几次项目联调中几乎都在第一轮就对上了加解密格式剩下的时间都花在了业务逻辑上。加密这东西看着玄学其实把参数对齐了剩下的都是体力活。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑