资讯动态

Vue前端AES加密实战:crypto-js实现敏感数据安全传输

发布时间:2026/10/21 18:53:06 来源:尧图企业网站定制
前端和后端之间传敏感数据很多团队第一反应是“我上了HTTPS就行了”但实际做过安全评审的都知道HTTPS只保护链路不保护业务数据本身。一旦日志被捞走、反向代理被碰、或者上游服务明文落库数据照样裸奔。我之前的项目里正好碰到类似的硬需求Vue前端要把用户敏感信息传到某后端服务链路要求不能出现明文于是就把AES加密这套方案完整落地了。这篇文章会把前端Vue用crypto-js做AES加密、后端做解密的完整过程讲清楚包括参数选择、代码封装、踩过的坑以及一些常规文档里不会写的东西。如果你正打算做前后端数据加密或者只是好奇AES在真实项目里怎么用这篇文章应该是能直接拿去参考的。1. 为什么要在前后端之间做AES加密1.1 这个方案解决的真实问题先说结论AES加密不是用来替代HTTPS的它解决的是HTTPS兜不住的那一层。几个实际场景你自己感受一下。第一前端页面在浏览器里跑用户输入的身份证号、手机号、银行卡信息如果直接用明文拼进请求体哪怕有HTTPS只要任何一层日志记录了请求体敏感字段就漏了。第二很多系统会加网关或者日志采集组件请求体经常会被整体打印这就是明文大范围暴露的高发区域。第三后端的数据库表如果也按明文存储一旦备份文件泄露那更是一锅端。当初项目组的做法是把“传输加密”和“存储加密”分开设计存储部分单独走库表加密传输部分就是用AES做业务字段加密。需求很明确前端在发送前把明文变成密文后端收到以后解密中间任何环节看到的都是不可读的密文串。1.2 对称加密为什么适合这个场景对称加密和非对称加密选型的时候确实纠结过一轮。RSA是非对称公钥加密、私钥解密好处是公钥可以随便丢私钥留后端前端不需要知道私钥。听起来很完美但问题是RSA加密的数据有长度限制2048位密钥最多只能加密约117字节的明文你要传一个几百字的表单得分片加密繁琐不说性能还慢。AES是对称加密加解密用的都是同一把密钥速度快适合批量数据单次能处理的数据量远大于RSA。密钥交给前后端各持一份前端加密后端解密。它的弱点也很直观密钥不能泄露一旦密钥出了前端代码仓库或者后端配置中心整个体系就废了。这个项目选AES还有一个原因前后端都有成熟的实现库前端是crypto-js后端无论是Node.js还是Java标准库直接支持不需要额外引入重量级依赖。整个链路做起来非常轻半天就能把代码跑通。1.3 方案落地后的整体架构整个加密链路按三个阶段划分。第一阶段前端在JS层调用封装好的AES工具函数把原始请求体里的敏感字段做加密生成的是Base64编码的密文。第二阶段走正常的HTTP请求请求体里携带的是密文字符串。第三阶段后端接口收到密文调用AES解密工具类还原出明文再做业务处理。从调用方视角看只是传了一个“看起来像乱码”的字符串加密细节全被封装在工具类里。从后端视角看解密逻辑也封装在服务端工具类里Controller层基本上无感。这套架构好处在于加解密逻辑都收敛在同一个模块后续要换密钥或者升级算法不用全工程到处改。2. AES加密的核心概念2.1 密钥、IV、分组模式AES本身是分组加密算法明文会被切成固定长度的块AES-128按16字节分块AES-192和AES-256也按16字节分块只是密钥长度不同。每个明文块独立加密但如果你直接每个块独立加密相同明文块会产生相同密文块这就有严重的模式泄露问题。所以实际使用时必须配合分组模式让每个密文块依赖于前一个块把明文规律打散。这里不得不提两个参数密钥和IV。密钥Key是真正的加密钥匙AES-128就是16字节AES-256就是32字节。IV初始化向量是给第一块明文做“随机扰动”的相当于给加密过程加一个初始偏移量每个分组模式对IV的处理不一样但IV的常见长度也是16字节。项目里选择的是AES-256-CBC组合32字节密钥16字节IVCBC分组模式。为什么是256而不是128纯属安全冗余考量既然前后端跑AES在性能上差异不大能上256就不留128的短板。2.2 ECB和CBC怎么选ECB是最简单的分组模式每个明文块独立加密不依赖其他块。它的致命伤在于如果明文里有两块内容相同密文里也会出现相同的块这样明文的排列规律就暴露了。最典型的例子就是加密一张图片用ECB模式加密之后图片轮廓还能隐约看出来。这种模式在真实项目里基本被废了。CBC模式是每个明文块先与前一个密文块做异或然后再加密。第一个明文块没有前一个密文块就用IV来异或。这样一来相同明文在不同位置、不同IV下加密出来的结果完全不一样明文规律被打乱了。代价是不能并行加密而且前一个块出错会影响下一个块但对前后端小数据量加密来说这个代价可以忽略。项目里用CBC而不是更先进的GCM主要考虑还是兼容性和实现成熟度。CBC是目前各种语言之间最容易对齐的AES模式加上PKCS7填充前后端基本不会出现“加密能过、解密不过”的问题。GCM虽然带认证能防篡改但跨语言实现时需要注意的细节更多第一次搞加解密不建议直接上。2.3 填充、编码、Base64AES要求明文长度必须是16字节的整数倍实际业务数据很难恰好满足比如加密一个“123456”只有6个字节所以必须填充。最常见的填充模式是PKCS7缺几个字节就补几个值为“缺少数”的字节。缺5个字节就补5个0x05如果明文刚好是16的倍数还要额外加一整块0x10否则解密端没办法判断末尾是不是填充数据。Java里写的PKCS5Padding和前端crypto-js里的Pkcs7实际它们在这个场景下是同一回事因为Java的PKCS5Padding在AES分组大小16字节的语境下底层实现跟PKCS7一致。这个知识点特别容易让第一次做AES的人迷茫我第一次对接时也绕了一下。编码环节加密结果是二进制字节流直接放进JSON会乱码所以要转成可打印字符串。最通用的方式是Base64前端加密后用toString()直接得到Base64串后端用Base64解码器还原字节数组再解密。整个链路统一走Base64基本不用考虑其他编码方案。3. 前端Vue工程实现AES加密3.1 引入crypto-js依赖前端库选了crypto-js它是一个非常成熟的纯JS加密库支持AES、DES、SHA系列等各种算法体积不算小但项目里只用到AES按需引入模块就好。安装命令很简单npm或者yarn都行npm install crypto-js这里有个坑crypto-js的版本差异会导致模块导入方式不一样。如果你用的Vue2配合较老的项目可能直接import CryptoJS from crypto-js而新版项目里这么做也没问题因为它默认导出了整个库。如果你只想引入AES模块可以这么做import CryptoJS from crypto-js;用整个库的导入方式在构建时体积会大一点但如果项目里已经有crypto-js其他算法在跑那就直接复用全局导入。我的项目里是统一用全局导入省得维护多个子模块路径。3.2 封装加密工具类前端不能把加密逻辑散落在各个页面组件里所有加解密必须收敛到一个工具文件统一出入口。我习惯建一个src/utils/crypto.js专门放AES加密相关逻辑后续要改密钥或者调整算法只动这个文件就够了。核心代码看起来是这样import CryptoJS from crypto-js; const AES_KEY your-32-byte-secret-key-0123456789abcdef; const AES_IV your-16-byte-iv-string; export function encryptByAES(plainText) { const key CryptoJS.enc.Utf8.parse(AES_KEY); const iv CryptoJS.enc.Utf8.parse(AES_IV); const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }几个细节说明一下。密钥长度必须严格匹配算法AES-256要求密钥是32字节如果字符串长度不够或者超过crypto-js不会直接报错但后端解密时会得到一串乱码或者直接异常这一点特别坑。IV固定是16字节不能随便写个8位字符串凑数。encrypted.toString()默认输出Base64格式后端拿到以后要按Base64解不要再去额外做URL编码除非你想把密文直接塞到URL的query参数里那又是另一种处理方式。3.3 请求拦截器集成工具类封装好以后怎么接入业务代码是个选择。最省事的做法是在axios请求拦截器里统一处理但有个前提只对指定的敏感字段做加密而不是对整个请求体加密。如果对整个请求体加密后端所有接口的入参模型都要改成接收密文改动面太大。我的做法是请求拦截器里遍历一个配置好的敏感字段列表只要请求参数里出现这些字段名就原地替换成加密后的密文。字段列表比如[phone, idCard, account]每个字段的值在发送前都会被加密。这一段代码大致长这样import { encryptByAES } from /utils/crypto; const sensitiveFields [phone, idCard, account]; service.interceptors.request.use((config) { if (config.data typeof config.data object) { const data { ...config.data }; Object.keys(data).forEach((key) { if (sensitiveFields.includes(key) typeof data[key] string) { data[key] encryptByAES(data[key]); } }); config.data data; } return config; });这里有几个坑需要特别提示。第一有些表单字段值是数字类型比如手机号存成了number你得先把数字转成字符串再加密否则crypto-js会做隐式转换加密出来的结果可能跟后端期望的不一致。第二GET请求的query参数也有敏感字段那就不能只在body里处理需要把query参数也走一层加密。第三如果字段值本身已经是密文比如编辑页面回显时后端返回的就是密文前端就不要二次加密这个要做一次状态判断否则后端解密出来就是个双重加密的乱码。4. 后端服务的解密实现4.1 Node.js版解密服务后端如果用的是Node.js不需要装第三方库直接使用内置的crypto模块就能完成解密。Node.js的crypto模块功能非常强唯一要注意的是API风格新版本支持createDecipheriv别再用已经废弃的createDecipher。解密函数封装如下const crypto require(crypto); const ALGORITHM aes-256-cbc; const AES_KEY your-32-byte-secret-key-0123456789abcdef; const AES_IV your-16-byte-iv-string; function decryptByAES(encryptedBase64) { const key Buffer.from(AES_KEY, utf8); const iv Buffer.from(AES_IV, utf8); const decipher crypto.createDecipheriv(ALGORITHM, key, iv); let decrypted decipher.update(encryptedBase64, base64, utf8); decrypted decipher.final(utf8); return decrypted; } module.exports { decryptByAES };这里必须要强调final()方法。很多新手写到这里只调用了update()发现解出来还是不完整的字符串或者直接报错原因就是final()才是把解密结果完整输出的最后一步。update()方法在读入密文数据时由于分组大小对齐的原因可能只是部分输出最后一点数据必须通过final()冲刷出来。Buffer.from(AES_KEY, utf8)特别关键。有人习惯直接传字符串给createDecipheriv比如crypto.createDecipheriv(ALGORITHM, AES_KEY, AES_IV)这种写法在Node.js 12之前的版本还能正常工作新版本里密钥和IV必须是Buffer类型否则会抛异常。为了统一我都是显式转Buffer。4.2 Java版解密实现后端的另一个常见技术栈就是Java。很多公司的服务端是Java Spring Boot跟Vue前端对接时解密逻辑用Java实现也很顺手。Java标准库自带的javax.crypto完全够用不需要引入额外的三方库。核心代码如下import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class AesUtil { private static final String ALGORITHM AES/CBC/PKCS5Padding; private static final String AES_KEY your-32-byte-secret-key-0123456789abcdef; private static final String AES_IV your-16-byte-iv-string; public static String decrypt(String encryptedBase64) throws Exception { byte[] encryptedBytes Base64.getDecoder().decode(encryptedBase64); SecretKeySpec keySpec new SecretKeySpec( AES_KEY.getBytes(StandardCharsets.UTF_8), AES); IvParameterSpec ivSpec new IvParameterSpec( AES_IV.getBytes(StandardCharsets.UTF_8)); Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decryptedBytes cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } }这里有个关键点Java里写的是PKCS5Padding前端crypto-js写的是Pkcs7但它们在AES/CBC模式下是兼容的。原理我在前面提过Java的PKCS5Padding在AES的16字节分组下实际行为就是PKCS7。如果你在Java端写成NoPadding那和前端就没法对齐了解密一定要用PKCS5Padding。cipher.doFinal()在密钥错误、密文被篡改、填充不合法等情况下都会抛异常异常类型是BadPaddingException或IllegalBlockSizeException。这里要提醒的是解密失败时不要直接把异常堆栈抛给前端应该统一转成业务异常返回一个通用的“数据解析失败”错误避免泄露内部实现细节。4.3 返回数据同样走加密链路一开始项目只做了前端请求加密、后端解密后来安全评审提了个问题接口响应里的敏感数据也是明文。比如用户详情接口返回手机号、身份证号前端页面直接渲染那中间链路照样能看到。所以后来把响应链路也加上了加密后端返回敏感字段前先加密前端拿到响应后解密。这里就要反向再写一个工具函数。前端解密时用同样的密钥和IV因为是对称加密加解密同源。后端加密则复用同一个解密函数里的密钥和IV只是把Cipher初始化模式改成ENCRYPT_MODE。Java后端响应加密的辅助方法如下public static String encrypt(String plainText) throws Exception { byte[] plainBytes plainText.getBytes(StandardCharsets.UTF_8); SecretKeySpec keySpec new SecretKeySpec( AES_KEY.getBytes(StandardCharsets.UTF_8), AES); IvParameterSpec ivSpec new IvParameterSpec( AES_IV.getBytes(StandardCharsets.UTF_8)); Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encryptedBytes cipher.doFinal(plainBytes); return Base64.getEncoder().encodeToString(encryptedBytes); }前端对应的解密函数也必须补上export function decryptByAES(encryptedBase64) { const key CryptoJS.enc.Utf8.parse(AES_KEY); const iv CryptoJS.enc.Utf8.parse(AES_IV); const decrypted CryptoJS.AES.decrypt(encryptedBase64, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); }注意decrypt方法返回的是WordArray对象要输出字符串必须用toString(CryptoJS.enc.Utf8)很多人在这一步卡住解出来是乱码就是因为没指定编码转换。5. 踩坑记录几个“解密失败”的真相5.1 密钥和IV不一致导致的诡异结果前后端密钥完全一致看起来是基本要求但实际项目里出问题最多的恰恰是这里。比如前端密钥末尾多了个空格肉眼根本看不出来后端密钥是标准32字节前端是33字节此时crypto-js还会用这个多一位的密钥去加密但后端解密时会直接报密钥长度错误。IV不一致就更隐蔽了。CBC模式第一个块会跟IV做异或运算如果IV不一致解密出来的第一个块必然是乱码后面的块可能正常也可能连带报填充错误。现实场景是前端把IV写成了另一个项目的IV后端还是老项目的IV两边接口联调时发现第一个字段解密出来是个空值或者一长串乱码。排查这类问题的思路是先在本地做一次自测前端加密一段固定的字符串把密文打印出来后端用同一段密文解密如果本地都过不了就别浪费时间联调了。5.2 Padding模式不匹配的问题前端Pkcs7和后端PKCS5Padding这对组合在实际工作中经常会遇到反向问题有人随意把后端改成NoPadding因为觉得反正数据不需要填充。这么一改解密出来的字符串末尾就会带着一堆\u0000或者奇怪的二进制填充符最典型的是字符串末尾多了几个不可见字符。如果确实要用NoPadding那就要求明文长度本来就是16的倍数但业务上很难保障。所以最稳妥的就是维持PKCS7/PKCS5Padding组合两边都不动这是最容易对齐的那条路。5.3 Base64传输中的加号问题前端生成的Base64字符串里可能带、/、这些字符。如果在传输过程中密文被放在了URL的query参数里没有做encodeURIComponent处理后端接收到的字符串里会被解析成空格/在部分网关里会被当作路径分隔符处理解密自然失败。解决方案很简单要么把密文放在请求体里而不是URL里要么拼接URL时统一做encodeURIComponent(encryptedStr)。后端取参时要记得URL解码一次Java里就是URLDecoder.decode(param, UTF-8)Node.js就是decodeURIComponent(param)。我在项目里遇到过一次线上问题排查了半下午最后发现就是号变成了空格。前端打印密文也正常后端日志打出来也正常但就是莫名其妙多了一个空格检查传输层编码才发现问题。5.4 加密字段类型与空值处理敏感字段如果是null、空字符串、或者纯数字处理逻辑一定要统一。前端加密空字符串时加密函数依然会输出一串有效密文后端解密出来是空字符串这没毛病。但如果你在前端先判断非空才加密空值就传了明文这就有逻辑漏洞。建议统一策略所有敏感字段不管是否为空都走同样的加密函数让全链路保持一致。数字类型也要注意。手机号如果前端存的是number加密函数会先转成字符串再加密后端拿到解密后是字符串“13800138000”前后端类型对不上可能导致数据校验出问题。建议前端在表单预处理阶段就把这类值统一转成字符串。6. 几点安全建议和项目心得6.1 密钥硬编码是最大的短板整个AES方案的阿喀琉斯之踵就是密钥管理。前端代码跑在浏览器里任何代码都可能被查看crypto-js加密逻辑会被直接看到密钥也会暴露在被压缩的JS文件中。所以必须清醒认识到前端加密只是提高攻击成本的“门锁”不是防破解的“保险柜”。项目里做的折中方案是分层管理密钥上线环境通过环境变量注入不写进前端源码仓库构建时替换占位符。后端密钥存储在配置中心不落普通配置文件。这样至少避免了“代码仓库泄露等于密钥泄露”的最坏情况。6.2 性能和体量的权衡AES加密对CPU开销不算大但要注意密文体积。Base64编码会把数据增大四分之一到三分之一如果你对整个请求体加密请求体积会明显变大。项目里只挑了敏感字段加密最多也就十几个字段对整体性能影响很小。如果加密字段特别多建议把所有敏感字段统一拼成一个JSON字符串加密后放入一个单独字段比如encryptedData传给后端。后端解密后从JSON中取字段。这么做不仅减少加解密调用次数还让网关注意到只有一个密文段日志被碰时暴露面更小。这个方案唯一要注意的是大字段拆包和超时配置如果密钥和加密数据量大后端接口的响应时间会有所增加。6.3 这套方案适合用在哪里回到标题本身Vue前端加密、后端解密AES实现。这套方案真正适合的项目类型是HTTPS已经上了但业务方有明确的数据安全审计要求敏感字段必须在传输链路中不可读。它适合中小型系统、前后端分离的业务后台、金融类Demo、需要快速演示加密效果的项目。如果项目要求防篡改、防重放建议后续再叠加消息认证机制比如CBC模式下再对密文做HMAC或者直接升级到GCM模式。因为AES-CBC本身不提供完整性保护密文被篡改以后解密端只能得到乱码却无法识别这是攻击行为还是传输损坏。我个人在实际落地中的体会是前后端加密最怕的不是算法选错而是两边标准没对齐。出问题的时候先确认密钥、IV、模式、填充、编码五个点至少能把九成以上的解密失败问题找到。先把工具类写好、参数统一约定好再谈安全增强和密钥托管这条路走下来是最稳的。

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

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

免费获取报价 →
↑