ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端JS加密算法实战:从MD5到AES/RSA选型与避坑指南

前端JS加密算法实战:从MD5到AES/RSA选型与避坑指南 1. 为什么前端要折腾加密——先聊需求场景做前端开发的朋友几乎都会遇到一个灵魂拷问客户说“密码不能明文传”产品说“接口不能被随便刷”后端同事甩过来一句“你们前端把参数加密一下”。于是你打开搜索引擎输入”JS加密算法”看到一堆MD5、AES、RSA却不知道该选哪个更不知道加密之后到底解决了什么问题。我最早接触这个主题是因为一个外包项目被安全测试报告打回了。报告上写着“登录接口密码明文传输存在泄露风险”。当时我的第一反应是就算前端加密了后端拿到的还是能解密的密文这有意义吗后来踩过几次坑才慢慢想明白——前端加密从来不是用来防真正高段位攻击者的而是用来防“随手抓包就能看到明文”这种低成本的暴露风险。换句话说它是在抬高攻击门槛而不是一劳永逸地解决问题。这篇内容适合谁一个是刚接触前端安全、被要求“加密一下”但不知道从何下手的新人另一个是已经在用某种加密库但不太清楚原理、想搞明白怎么选型和避坑的开发者。我会把常用的几种JS加密算法、它们的适用场景、实际代码写法以及我在项目里踩过的坑一次性讲清楚。2. 主流JS加密算法逐个拆解2.1 摘要算法MD5与SHA系列摘要算法也叫哈希算法它的特点是“不可逆”——把任意长度的数据计算成一个固定长度的字符串理论上无法从结果反推原文。这几年我见到的很多前端项目第一个接触的加密算法基本都是MD5因为用法太简单了import CryptoJS from crypto-js; const hash CryptoJS.MD5(hello world).toString(); console.log(hash); // 5eb63bbbe01eeed093cb22bb8f5acdc3调一个函数就出结果看起来很美。但MD5有个非常致命的问题它已经被证明存在碰撞漏洞而且因为计算速度快很容易被彩虹表暴力破解。如果只是用来做文件完整性校验、生成缓存键MD5到现在依然够用但如果用来存密码或做签名我强烈建议换SHA-256至少别再用裸MD5。SHA系列里面我们最常碰到的三个是SHA-1、SHA-256和SHA-512。SHA-1在2017年被Google成功碰撞后就走下神坛了现在做前端签名我基本都是用SHA-256。它的用法和MD5几乎一样const hash CryptoJS.SHA256(hello world).toString(); console.log(hash); // b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcdde9这里要多说一句很多后端把密码字段起名叫做password_hash而不是password就是这个原因——摘要不是加密它不追求可逆追求的是“你改了任何一个字节结果天差地别”的雪崩效应。2.2 对称加密AES的前端实现AES是目前使用最广泛的对称加密算法。所谓对称就是加密和解密用的是同一把密钥。它的典型使用场景是你的页面需要传给后端一段结构化数据后端拿到后用同一把密钥解密读取或者反过来后端下发一段敏感配置前端用密钥解密后再使用。前端用CryptoJS实现AES加密网上流传的写法五花八门我见过最省事的版本是这样function encrypt(plainText, secretKey) { const key CryptoJS.enc.Utf8.parse(secretKey); const iv CryptoJS.enc.Utf8.parse(1234567890123456); const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }但如果你照着抄大概率会在联调时踩坑。最典型的问题出在编码CryptoJS的AES.encrypt如果第一个参数直接传字符串内部会按UTF-8处理但如果后端Java用的是AES/CBC/PKCS5Padding密钥和IV必须以字节数组传入而且两边对密钥长度的约定必须一致128位就是16字节256位就是32字节。很多初次对接的同事前端用16字节密钥、后端用32字节两边各写各的然后互相怀疑对方代码有bug。所以我在实战里更推荐一套明确的约定密钥统一用32字节的UTF-8字符串IV固定为16字节模式用CBC填充方式用PKCS7Java端PKCS5和PKCS7在AES里效果等价这个名称差异也是联调时最容易吓到人的点。2.3 非对称加密RSA的基本用法RSA的核心特点是“一对钥匙”——公钥加密、私钥解密。公钥可以公开放在前端代码里也不怕被人看到因为拿到公钥的人没法解密私钥只有后端持有。这在解决“密钥如何安全传输”这个老难题上提供了本质不同的思路。前端使用RSA加密最常用的库是jsencryptimport JSEncrypt from jsencrypt; const encryptor new JSEncrypt(); encryptor.setPublicKey(-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----); const encrypted encryptor.encrypt(sensitive data);这个逻辑很清晰前端用公钥加密后端用私钥解密。但有个实际限制必须说——RSA加密的明文长度有限制1024位密钥最多只能加密117字节2048位密钥最多加密245字节。如果你拿它加密一长串JSON它直接报错或者返回空。我遇到过一个真实案例同事把整个表单数据用RSA加密结果上线第一天就收到用户反馈“表单提交失败”排查半天才发现是数据超长。解决思路通常是混合方案用RSA加密一个临时生成的AES密钥再用AES加密实际数据。这就是所谓的“数字信封”方案既解决了RSA长度限制又拿到对称加密的高性能。3. 实战落地从选型到代码3.1 先搞明白你到底需要加密还是签名选加密算法之前先回答一个问题你的目标是“让别人看不懂”还是“让别人改不了”这两个诉求对应完全不同的技术路线。前者用加密算法比如AES、RSA目的是保密性后者用签名算法比如HMAC-SHA256目的是完整性。很多业务场景其实只需要签名不需要加密。比如你的页面本来就要把用户名、时间戳这些非敏感字段传给后端你真正担心的是请求被人篡改、或者被人伪造那最合适的做法是签名而不是把整段数据加密成密文。HMACHash-based Message Authentication Code本质上就是“带密钥的哈希”。前端用密钥把参数串计算成一个摘要后端用同一个密钥重新计算对比是否一致。只要密钥不泄露改动任何一个参数都会导致签名对不上function sign(params, secretKey) { const sortedKeys Object.keys(params).sort(); const queryString sortedKeys .map(key ${key}${params[key]}) .join(); const signature CryptoJS.HmacSHA256(queryString, secretKey).toString(); return signature; }签名前先把参数排序这是我踩过坑换来的经验。如果两端遍历参数的顺序不一致同样的数据会算出完全不同的签名。所以约定好“先排序、再拼接、最后签名”这套流程是联调的第一步。3.2 密码传输的完整方案设计登录密码是前端加密最常见、也最应该做好的场景。我在生产环境采用过的方案是这样的第一步用户点登录时前端生成一个随机字符串作为盐值和后端通过配置接口下发的会话ID一起参与运算第二步用SHA-256对“密码盐值”做摘要第三步再用后端下发的公钥对这个摘要做RSA加密然后把密文和时间戳一起提交。这样设计的逻辑是摘要运算保证了同一个密码在不同会话中产生的密文不同防止重放攻击RSA加密保证即使抓包看到密文也无法还原出密码摘要时间戳让后端可以拒绝过期的请求。整个过程看起来复杂但每一步都有明确的目的不是炫技。不过我必须强调一个原则密码校验的最终权威一定在后端数据库。前端做的所有动作本质上是防止密码在传输链路上以明文形式暴露而不是替代后端的安全存储。后端拿到密文解密后要再和自己的存储策略做比对比如用bcrypt这类专门设计的慢哈希来存储。3.3 接口签名与防重放的配合只做签名还不够因为攻击者可以把你的请求原封不动地重放。我在一个支付相关的项目里就遇到过这种情况——等额充值接口被人用脚本反复提交虽然数据本身没被篡改但业务上造成了损失。解决方案是引入时间戳和随机数。前端每次请求带上timestamp和nonce签名时把它们也计算进去后端在验签通过后检查时间戳是否在5分钟内再把nonce存入一个短期缓存。同样的nonce出现两次就拒绝请求。这个双保险的思路在很多开放平台API设计里都能看到称之为防重放机制。代码实现上前端只需要在请求拦截器里统一处理function buildSignedRequest(data, secretKey) { const timestamp Date.now(); const nonce CryptoJS.lib.WordArray.random(16).toString(); const payload { ...data, timestamp, nonce }; return { ...payload, sign: sign(payload, secretKey) }; }这里有个容易被忽略的小技巧nonce不只是随机字符串它在后端是会被缓存比较的所以必须保证唯一性。用CryptoJS.lib.WordArray.random(16)生成足够长的随机数比用Math.random()安全得多因为后者理论上是有规律可循的。4. 常见坑与排查技巧4.1 编码不一致导致两边结果对不上这是加密切磋中最常见的问题没有之一。前端用CryptoJS.enc.Utf8.parse()把字符串转成字节数组后端用getBytes(UTF-8)两边看着都是“同一个字符串”但结果就是不一致。排查步骤我一般这样走第一确认密钥、IV、明文的编码格式在两端完全一致统一使用UTF-8第二确认AES的模式和填充一致CBC对应CBCPKCS7对应Java的PKCS5第三确认加密结果的表示方式一致——前端toString()默认输出Base64字符串后端如果用了Hex解码必然对不上。我还碰到过一个特别坑的情况前端把密文encodeURIComponent了一下再传给后端后端忘记decodeURIComponent导致密文中包含的号被当成空格解密直接失败。从那以后我所有涉及密文传输的参数都强制要求用Base64并做URL编码后端也把解码步骤放在验签之前。4.2 密钥管理没有绝对安全的前端密钥这是前端加密最让人沮丧的现实密钥放在JavaScript代码里本质上是公开的。任何懂一点浏览器调试的人打开开发者工具找到压缩后的JS文件搜索密钥关键词分分钟就能扒出来。所以我对密钥管理有三条实战心得第一条区分“可暴露密钥”和“核心密钥”。公钥、用于生成签名的appKey这类暴露了不至于全盘崩溃私钥、数据库加密密钥这类绝对不允许出现在前端代码里。第二条不要把密钥硬编码在源码中。可以通过后端接口下发或者放在构建配置里通过环境变量注入尽量增加被直接发现的难度。第三条所有前端加密方案都要假设“密钥可能泄露”在后端做最后的防线。我见过一种比较稳妥的做法后端按照用户和会话动态派生密钥每个用户拿到的密钥都不一样即使某一个密钥泄露了影响范围也被控制在单用户维度。注意前端加密的目标是提高攻击成本不是构建绝对安全的边界。任何“只要前端加密就安全了”的想法都会在遇到真正较真的攻击者时付出代价。4.3 性能问题大文件加密与耗时操作有人习惯把整个表单数据一股脑加密后提交遇到大字段时页面明显卡顿体验极差。其实前端加密的耗时和CPU占用是实打实的尤其是在低端移动设备上RSA加密超过几百字节的数据会让人明显感觉到卡顿。我的建议是遵循最小化原则只加密真正需要保护的敏感字段而不是加密整包数据。比如登录接口只需要加密密码字段其他账号、时间戳走常规参数订单提交接口只需要加密金额和收货信息商品明细列表保持明文。这样既满足了安全需求又不会让用户等到崩溃。如果确实需要加密较大的数据用AES而不是RSA。AES的速度比RSA快几个量级128位密钥的AES加密1MB数据通常只需要几十毫秒而RSA连1KB都吃力。这也是数字信封方案在大数据量场景下成为主流的原因。4.4 常见问题速查表问题现象可能原因解决方案解密出来是乱码两端编码格式不一致统一UTF-8编码检查IV长度AEAD解密报错使用了mode.CBC但后端要求GCM更换模式GCM需要额外传tagRSA加密空字符串或报错明文超过密钥长度限制改用混合加密方案签名联调总不一致参数拼接顺序不统一约定先排序、再拼接、后签名密文传输后解密失败URL编码导致号被转义对密文做encodeURIComponent后端解码同一个密文每次相同没有加随机IV或盐值使用随机IV并随密文一起传输5. 工具选择与工程化建议说到JS加密库我实际用过并且敢推荐的不多。crypto-js是最普及的基本上MD5、SHA、AES、HMAC样样都有文档多、示例全适合快速上手jsencrypt专精RSA接口简单但开发状态有时候让人不太放心Node.js环境直接用内置的crypto模块就够了Web端用Web Crypto API也是一种更贴近原生的选择就是API设计对新手不太友好。如果你用的是Vite或Webpack这类现代构建工具建议把加密库按需引入别一上来就整包引入。crypto-js的包体积不小我在一个Vite项目里因为整包引入导致首屏包体积增加了将近200KB后来改成只引入需要的子模块优化效果立竿见影import SHA256 from crypto-js/sha256; import HmacSHA256 from crypto-js/hmac-sha256;另一个工程化建议是把所有的加解密操作封装到一个独立的模块里对外只暴露业务方法比如encryptPassword(password)、signRequest(data)。这样后面要换算法、换密钥、改逻辑只需要动一个文件其他业务代码完全不用跟着变。我在两个项目里都是这么做的后面接手的人都觉得清爽。6. 聊聊我认为最值得记住的几点写了这么多算法和代码最后还是想说说那些代码之外的东西。我在实际项目中反复感受到一个矛盾非安全专业的前端工程师被要求承担一部分安全责任但很多时候我们并不知道自己写的加密是不是真的有用。说实话前端加密这件事真正起作用的部分往往不在算法本身而在于你对整个链路有没有想清楚——密钥从哪来、密文在哪验证、泄露了能影响多大范围、后端有没有兜底。以我个人经验来说上手这件事最好的路径是先从签名做起因为它简单、落地快、能真实地防住一部分篡改行为然后再尝试AES加密一些敏感字段逐渐理解密钥和编码的细节最后再碰RSA和混合加密。一上来就想搞一套完美的非对称加密方案大概率会在密钥管理和联调上拖垮自己。最后分享一个小技巧加密代码上线前用浏览器开发者工具把Network面板打开随便发一个请求看看Payload里的加密字段长什么样。如果你自己都无法从密文联想到原始数据那基本说明至少做到了“不算太差”的水平。而真正的安全还需要后端、运维、产品一起努力前端只是第一道门不是最后一道墙。
返回列表