☰
Vue前端AES加密实战:crypto-js实现敏感数据安全传输
2026/10/21 18:52:55 网站建设 项目流程

前端和后端之间传敏感数据,很多团队第一反应是“我上了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字节IV,CBC分组模式。为什么是256而不是128?纯属安全冗余考量,既然前后端跑AES在性能上差异不大,能上256就不留128的短板。

2.2 ECB和CBC怎么选

ECB是最简单的分组模式,每个明文块独立加密,不依赖其他块。它的致命伤在于:如果明文里有两块内容相同,密文里也会出现相同的块,这样明文的排列规律就暴露了。最典型的例子就是加密一张图片,用ECB模式加密之后,图片轮廓还能隐约看出来。这种模式在真实项目里基本被废了。

CBC模式是每个明文块先与前一个密文块做异或,然后再加密。第一个明文块没有前一个密文块,就用IV来异或。这样一来,相同明文在不同位置、不同IV下加密出来的结果完全不一样,明文规律被打乱了。代价是不能并行加密,而且前一个块出错会影响下一个块,但对前后端小数据量加密来说,这个代价可以忽略。

项目里用CBC而不是更先进的GCM,主要考虑还是兼容性和实现成熟度。CBC是目前各种语言之间最容易对齐的AES模式,加上PKCS7填充,前后端基本不会出现“加密能过、解密不过”的问题。GCM虽然带认证,能防篡改,但跨语言实现时需要注意的细节更多,第一次搞加解密不建议直接上。

2.3 填充、编码、Base64

AES要求明文长度必须是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、模式、填充、编码五个点,至少能把九成以上的解密失败问题找到。先把工具类写好、参数统一约定好,再谈安全增强和密钥托管,这条路走下来是最稳的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询