
1. AES加密在Vue项目中的用处与基础认知做前端开发几年后你会发现一个很现实的问题很多项目里用户的敏感信息密码、手机号、身份证号经常是明文从浏览器发到服务端的。之前有段时间我做了一个后台管理系统登录接口抓包一抓一个准密码就明晃晃躺在Payload里。虽然大部分正规系统都上了HTTPS但内网环境、测试环境、某些特殊网关下明文传播还是让人不放心。AES加解密就是在这个场景下被频繁拿来讲的话题。AESAdvanced Encryption Standard高级加密标准是目前应用最广泛的对称加密算法之一。所谓“对称”就是加密和解密使用同一把密钥。前端在拿到原文后用密钥加密成密文后端拿到密文后用同一把密钥还原出原文。它的优势是速度快、实现成熟、生态丰富特别适合数据量大、实时性要求高的接口交互场景。在Vue 3项目里做AES加解密核心就四件事选一个好用且稳定的加密库第三方库或浏览器原生能力都行封装一个随时可调用的加密/解密工具模块在需要的地方调用比如表单提交前加密、Axios拦截器统一处理处理好密钥、偏移量IV、字符集、填充方式这些细节否则前后端对不上就会出现一堆诡异问题这里要先把一个观念端正前端加密并不等于绝对安全。因为密钥在前端代码里经过构建打包后理论上还是可以被有心人从源码中分析出来。它的核心价值是避免“明文裸奔”、增加数据被截获后的破解成本以及满足部分等保规则或项目组内部的安全规范要求。真正彻底的传输安全还是要依靠HTTPS这类传输层加密方案。所以这篇文章讲的是一套“在已有HTTPS基础上给交互数据再加一道防护”的实践而不是让你把前端加密当作唯一保命手段。适合谁来读正在做Vue 2/Vue 3项目想要给接口数据加解密或者已经在做但遇到密文解不开、前后端对接不上、解密乱码等问题的朋友。下面这部分我会从算法选型讲起再到工具封装、项目集成、常见故障排查整个链路一次讲透。2. AES算法关键概念与模式选择2.1 分组、密钥长度、填充方式到底怎么回事AES是分组加密算法它把明文按固定长度切块处理。每个块的大小是128位也就是16字节。如果明文长度不是16的倍数最后一个块就需要填充。Java里默认的填充是PKCS5Padding而JS生态里更常见的是PKCS7。严格来说PKCS5只针对8字节块但因为结果一致大家在实际使用中基本混着叫。CNG、.NET、Java、Node.js里这套填充逻辑都是兼容的前提是两端要统一。密钥长度方面AES支持三种128位16字节、192位24字节、256位32字节。Vue项目里用得最多的是AES-256密钥就是32个字符的字符串。虽然密钥越长越难暴力破解但对普通业务系统来说128位已经足够256位属于“多加一道心理防线”。需要提醒的是密钥长度必须与算法匹配16字节对应AES-12832字节对应AES-256长度错了加解密直接失败。这个问题在我刚开始用crypto-js时踩了很大的坑后面常见问题部分会详细说。2.2 ECB、CBC、CTR模式的差异以及为什么我更推荐CBCAES有多种工作模式Vue项目里最容易碰到的是ECB和CBC。ECB模式最简单每个明文块独立加密相同明文块会生成相同密文块。因为没有随机性容易被统计分析一般不推荐用于业务数据加密。但因为它不需要传IV很多老系统或后端框架默认用的都是它遇到“别人传过来的密文解不出来”时先看对方是不是ECB模式。CBC模式每个明文块先跟前一个密文块做异或运算再加密。第一个块要和随机生成的初始化向量IV异或。同样的明文、不同IV密文完全不一样。这种模式下密文更随机安全性更高是目前最通用的做法。CTR模式把计数器加密后和明文异或支持并行计算、不需要填充。在实时音视频、流式加密场景中用得多普通业务接口反而不常见。实际项目里我更推荐CBC模式加随机IV。原因很简单一是后端支持度最高Java、Python、Go里都有原生实现二是随机性带来的抗分析能力明显比ECB强三是在Vue里实现起来并不复杂就是多生成一个16字节的向量字符串而已。IV生成的常用方式import CryptoJS from crypto-js; const iv CryptoJS.lib.WordArray.random(16); // 生成16字节随机向量 const ivStr CryptoJS.enc.Base64.stringify(iv);这里生成的IV字符串每次加密都不同。需要注意解密时必须拿到同一个IV否则解不开。所以常见做法是加密后把IV拼到密文里一起传给后端或用固定IV。两种方案都有实践场景第一部分选型时就可以思考项目适合哪种。你项目如果只有三五个接口需要加密用固定IV最简单如果是动态数据频繁加解密随机IV更安全但前后端要多约定一个“IV怎么传递”的协议。2.3 字符集和编码为什么说Base64是前后端通用的“翻译官”AES加密出来的结果本质是一串二进制数据不能直接在JSON里传输更不能作为URL参数传递。所以前端往往会先把密文做一次编码让不可读的二进制变成可打印的字符串。目前最通用的就是Base64编码。后端拿到Base64密文后需要先解码成字节数组再做AES解密。前端代码里加密完直接输出Base64字符串就是下面这个效果const cipherText CryptoJS.AES.encrypt(plainText, secretKey, { iv: ivWordArray, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString();注意crypto-js的toString()默认输出的就是Base64字符串。我之前见过有人用CryptoJS.enc.Utf8.stringify去转结果解出来全是乱码因为二进制转字符串时必须指定正确的编码不能随便拿个编码格式硬转。明文编码也同样重要。前端拿到用户输入的普通字符串加密前要先转成UTF8的WordArray否则中文字符或Emoji符号会在加密后解密时出现“Malformed UTF-8 data”或乱码。crypto-js默认处理字符串时会按UTF8编码处理所以直接把字符串传给encrypt方法通常没问题但如果是从后端接过来的内容再加密时最好显式指定一下const utf8WordArray CryptoJS.enc.Utf8.parse(plainText); const encrypted CryptoJS.AES.encrypt(utf8WordArray, secretKey, config);这种显式处理能在源头上规避一批难以排查的编码问题。3. Vue 3项目安装依赖与加密工具类封装3.1 为什么选crypto-js而不是Web Crypto API浏览器其实自带一套原生加密能力叫Web Crypto API支持AES-CBC、AES-GCM等算法。它的优点是安全性高、走浏览器底层实现不需要额外引入第三方库。但实际用起来有几个痛点原生API完全基于Promise调用链比较绕虽然不至于复杂但封装起来比crypto-js多几步。原生API要求密钥必须是CryptoKey对象不能直接传一个字符串密钥密钥导入导出比较啰嗦。兼容性或特殊环境比如某些WebView下表现不稳定现场调试成本高。反观crypto-js直接在项目里install之后就能用API设计得很贴近封装好的业务需求。字符串、WordArray、Base64互转都是现成的代码写起来很顺手。Vue项目里目前用的最多的基本就是它。另一个选择是jsencrypt但它主要做RSA非对称加密RSA和AES的应用场景不一样AES适合大批量数据的加解密RSA适合加密密钥这类短数据。一般接口数据量都不小AES更合适。我自己的选型建议是Vue普通业务系统团队熟悉JS选crypto-js开发效率高。需要高强度安全且能接受异步化改造用Web Crypto API。有密钥安全分发需求可以和RSA配合AES加密业务数据RSA加密AES密钥。3.2 安装与目录规划创建Vite Vue 3项目后在项目根目录执行npm install crypto-js如果你用的是TypeScript推荐同时安装类型声明文件npm install types/crypto-js --save-dev安装完成后在src/utils目录下创建一个crypto.ts或crypto.js。目录结构大概长这样src utils crypto.ts views Login.vue UserInfo.vue api request.ts密码学相关的工具函数集中放在utils下不散落在组件里。组件里只负责调用不在业务层塞一坨加密逻辑。这样后面要更换算法、调整模式、统一改密钥时只需要改封装的这一处文件。3.3 一个可直接抄走的加密工具封装以CBC模式为例完整的crypto.ts文件如下import CryptoJS from crypto-js; // 密钥与IV统一从环境变量读取不要硬编码在源码里 const SECRET_KEY: string import.meta.env.VITE_APP_SECRET_KEY || default-dev-key-2024; class CryptoHelper { private key: CryptoJS.lib.WordArray; private iv: CryptoJS.lib.WordArray; constructor(keyStr: string, ivStr?: string) { // 密钥必须符合长度要求32个字符对应AES-256 this.key CryptoJS.enc.Utf8.parse(keyStr); // IV必须是16字节可以固定传也可以随机生成 this.iv ivStr ? CryptoJS.enc.Utf8.parse(ivStr) : CryptoJS.lib.WordArray.random(16); } /** * 加密返回Base64字符串 */ encrypt(plainText: string): string { const encrypted CryptoJS.AES.encrypt(plainText, this.key, { iv: this.iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); } /** * 解密接收Base64密文返回UTF8字符串 */ decrypt(cipherText: string): string { const decrypted CryptoJS.AES.decrypt(cipherText, this.key, { iv: this.iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return decrypted.toString(CryptoJS.enc.Utf8); } /** * 加密对象序列化后加密 */ encryptObject(obj: Recordstring, unknown): string { return this.encrypt(JSON.stringify(obj)); } /** * 解密为对象 */ decryptObjectT(cipherText: string): T { const jsonStr this.decrypt(cipherText); return JSON.parse(jsonStr) as T; } /** * 随机生成IV字符串配合随机IV方案时使用 */ static generateIv(): string { return CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Base64); } } export const cryptoHelper new CryptoHelper(SECRET_KEY);几个关键点提一下密钥用环境变量读取是为了不把敏感字符串直接写进业务代码里。但注意前端环境变量在构建时会被打包进产物所以VITE_APP_SECRET_KEY并不能做到真正意义上的机密只是让源码仓库里不出现一眼可见的密钥。真正要紧的密钥建议由后端接口临时下发。类封装的好处是后面想换成随机IV方案时可以在构造函数里动态传入IV不需要大改。这个设计我当时是用了几次后才体会到一开始写的是纯函数后来需求变成不同接口用不同IV纯函数就不好扩展了。decryptObject方法常用于解密后端下发的用户资料、配置信息。JSON.parse如果报错多半是解密结果空了要回到编码/模式勘误的方向排查。4. Vue 3组件与接口层的完整集成实践4.1 登录表单加密最典型的落地场景假设登录接口需要把密码加密后再提交。在Login.vue里引入上面封装的cryptoHelper注意所有敏感数据加密前都要先“字符串化”script setup langts import { ref } from vue; import { cryptoHelper } from ../utils/crypto; import { loginApi } from ../api/auth; const username ref(); const password ref(); const handleLogin async () { if (!password.value) return; // 密码加密后再放进请求体 const encryptedPassword cryptoHelper.encrypt(password.value); const payload { username: username.value, password: encryptedPassword, }; try { const res await loginApi(payload); console.log(登录成功, res); } catch (error) { console.error(登录失败, error); } }; /script这段逻辑很简单但有一个容易被忽略的细节用户名为什么没加密倒也不是不能加密只是很多后端系统拿用户名做日志检索、风控匹配时需要明文或哈希后的值。如果整个payload全部加密后端日志、监控工具全变成天书排查问题会很困难。所以我的实践经验是能识别数据类别的字段如用户名、邮箱尽量用明文传输或做脱敏处理真正需要保护的密码、身份证号、手机号、银行卡号等再做字段级AES加密。4.2 Axios拦截器统一加解密更推荐的做法如果项目里有几十个接口都需要加密是不是每个请求都要调用一次encrypt显然不是。用Axios拦截器在全局统一处理请求和响应才是正路。在api/request.ts中做一次封装import axios from axios; import { cryptoHelper } from ../utils/crypto; const request axios.create({ baseURL: import.meta.env.VITE_APP_API_BASE_URL, timeout: 15000, }); // 请求拦截器对业务数据整体加密 request.interceptors.request.use((config) { if ( config.method post || config.method put || config.method patch ) { const data config.data; if (data ! undefined data ! null) { config.data { // 关键定约后端按这个字段取密文 data: cryptoHelper.encrypt(JSON.stringify(data)), }; } } return config; }); // 响应拦截器尝试解密后端返回的密文 request.interceptors.response.use( (response) { const res response.data; // 约定加密响应带encrypted标志字段为payload if (res res.encrypted typeof res.payload string) { try { const decrypted cryptoHelper.decrypt(res.payload); response.data JSON.parse(decrypted); } catch (e) { console.error(响应解密失败, e); } } return response; }, (error) Promise.reject(error) ); export default request;使用全局拦截器后的效果是业务代码完全感觉不到加解密的存在视图层照常写JSON数据到了网络层自动变密文返回时自动还原成JSON。这套方案维护成本最低也最适合团队协作。不过它有一个前提后端必须愿意和前端约定好统一的报文结构否则就不要做整体加密老老实实回到字段级加密的方案上。4.3 带随机IV的报文扩展前端某次踩过坑之后我将标准升级为随机IV方案报文结构多了一个iv字段{ encrypted: true, iv: Base64编码的随机向量, payload: Base64编码的密文 }对应的工具类加一个方法encryptWithIv(plainText: string): { iv: string; payload: string } { const iv CryptoJS.lib.WordArray.random(16); const encrypted CryptoJS.AES.encrypt(plainText, this.key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return { iv: CryptoJS.enc.Base64.stringify(iv), payload: encrypted.toString(), }; }为什么有的团队愿意这么折腾因为固定IV的情况下相同的明文永远生成相同的密文攻击者可以从密文重复性上推测出“用户是否输入了相同值”。随机IV则每次加密结果都不一样整体更抗重放和统计。代价是后端要多读一个IV字段解密前先Base64解码回16字节向量。我接触过的项目里金融类、政务类的系统大多走这种方案普通内部系统一般没必要。4.4 vite环境变量配置密钥不要随手写进代码在项目根目录创建.env.development和.env.production两个文件VITE_APP_SECRET_KEYyour-32-bit-secret-key-here-123456 VITE_APP_IVyour-16-bit-iv-value然后在crypto.ts中通过import.meta.env读取。需要注意Vite的环境变量只有以VITE_开头的才会暴露给客户端取不到变量时页面会直接报错或者解密结果为空。保险做法是在封装的默认值上做个兜底但默认值本身只能用于本地开发生产环境必须通过部署平台注入真实的密钥。我见过有人把密钥写死在代码里然后打包完把js上传到服务器结果有次做前端代码评审时直接在压缩后的bundle里搜到了密钥明文。那之后我给自己定了一条规矩任何密钥类字符串不经过环境变量、不经过构建配置就不允许出现在业务源码中。5. 前后端联调中的约定、安全边界与常见坑5.1 前后端必须对齐的四要素AES加解密能不能一次调通往往就看下面四个点有没有对齐参数常见值说明算法AES-128 / AES-256密钥长度必须匹配前后端必须一致模式CBC / ECB / CTR不一致则解密直接失败填充方式Pkcs7 / ZeroPaddingJava默认PKCS5与Pkcs7实际一致ZeroPadding需要自行去除末尾0字符集UTF-8 / Base64明确哪个环节用UTF8哪个环节用Base64强烈建议在联调文档里把这四项写成一行字段后端同学拿到后直接按这组参数配置。我遇见过的“密文解密出来是一串base64”“密文是空的”“报错Cannot read properties of undefined”这类问题十有八九都是这四个点里有哪一项没对齐。5.2 密钥管理与安全边界把话说透前端AES最大的争议是“密钥都暴露给用户了加密还有什么意义”。我的理解是前端能把很多低水平攻击者挡在门外但它不是银弹。一个整日正经搞安全的团队不会只用前端加密而是会做多层防护。不过即使不指望前端加密解决全部问题也不能随便把密钥写死在代码里。比较稳妥的思路将基础密钥存在后端配置中心前端登录时通过一个开白名单的接口临时获取后续接口用会话内密钥加解密。使用RSAAES的混合加密方案先用后端公钥加密AES密钥把密文传过去后端用私钥解出AES密钥后续通信走AES。这样AES密钥每次会话都可变化就算某一次密钥被截获影响面也会小很多。敏感数据加密后日志中不要打完整密文避免大量可回溯的密文数据被收集。token和加密密钥尽量不要放在同一个存储容器里。localStorage一旦被XSS攻击拿到两个都丢了那加密就形同虚设。5.3 常见问题与排查技巧实录我自己在几个项目里反复遇到下面这些问题整理成一张速查表方便直接对号入座现象可能原因解决方向解密后字符串为“Malformed UTF-8 data”密钥长度不对、密文不是有效Base64、IV不匹配检查密钥长度是否32位确认密文是否Base64确认IV是否一致解密出来是一串奇怪的符号解密出来的字节被当成某种编码强制用CryptoJS.enc.Utf8.toString()输出AES加密结果每次都不一样使用了随机IV这是正常现象同时将IV传给解密方后端Java解不出来模式或填充方式不一致Java端和解密端字符集不一致统一CBC/Pkcs7Java用PKCS5Padding确认字符串直接UTF8密文传输中出现、/、后端报错Base64字符在URL中会被转义使用encodeURIComponent处理或自定义Base64URL安全处理Java端报“Given final block not properly padded”密钥不对、密文被截断、填充方式不匹配检查密文完整性统一填充方式前端解密报错但后端可以解密文用了hex编码而前端在按Base64解确认后端输出编码格式加密后的串特别长PKCS7填充及Base64膨胀正常现象16字节块要补齐不要手动去截断5.4 排查方法论一步步缩小问题圈如果你遇到的是一个复合怪问题不要急着在任何一端的代码里狂加日志。按照下面顺序排查能省一半时间先用一个固定明文“hello aes 2025”在前端加密把密文在浏览器控制台打出来。拿同一份密文去后端写一个最简明的解密方法可以用在线工具先验证你的参数看是否能还原。如果后端点解不开多半是参数不一致如果后端点解开了说明问题出在完整请求链路上。检查Axios请求里有没有被二次编码、有没有被拦截器改动过data结构。解密输出用console.log打印在控制台截图给后端对比两边对着同一个密文/明文调问题很快就能定位到某一层。这个方法帮我调通过三个不同后端技术栈的项目对我来说比任何框架层面的调试技巧都实用。6. 加密工具类的进阶扩展思路6.1 支持多种加解密方式切换实际项目中经常出现“一部分接口用AES一部分接口走明文”的情况。工具类里可以加两个switch方法type EncodeMode plain | aes; function requestDataFactory(data: unknown, mode: EncodeMode) { if (mode plain) return data; return cryptoHelper.encrypt(JSON.stringify(data)); }尤其在老项目改造阶段服务和客户端是一批一批切换的。保留一个明文透传的开关能让灰度发布平滑很多。6.2 参数签名配合AES防止篡改如果只想防篡改而不过度加密全部数据可以引入“AES加密签名”双机制。对关键参数生成一段签名串比如时间戳随机数密钥的SHA256摘要后端校验签名通过后才认为数据可信。这样不是所有字段都要加密但相比完全不设防护要稳妥不少。这个方案在不需要全报文加密又希望避免参数被随意改动的场景中很常用。6.3 复用性与组件化封装一个加密输入框有段时间项目里多个表单都要输入手机号、身份证号我在每个组件里都去调一次加密方法后来觉得太繁琐。于是做了一个轻量的加密输入组件template input :valuedisplayValue inputhandleInput :placeholderplaceholder / /template script setup langts import { ref } from vue; import { cryptoHelper } from ../utils/crypto; const props defineProps{ modelValue: string; placeholder?: string; }(); const emit defineEmits{ (e: update:modelValue, value: string): void; }(); const displayValue ref(props.modelValue || ); function handleInput(e: Event) { const raw (e.target as HTMLInputElement).value; displayValue.value raw; // 记下明文到v-model提交时统一加密 emit(update:modelValue, raw); } // 暴露加密后的值供父组件提交时使用 function encryptValue(): string { return cryptoHelper.encrypt(displayValue.value); } defineExpose({ encryptValue }); /script组件化之后的好处是加密逻辑收敛到组件内部业务页面只用记着一句话“输入时给明文提交前取密文”。如果后续需要升级为随机IV方案也只需要在组件内部调整调用方式不涉及页面代码改动。7. 写在最后几个实操中的真实体会这几个月的实践里我最大的感受是AES加解密本身并不难难点都在“约定”上。加密算法、模式、填充、字符集、IV传递方式、报文结构任何一项没约定明白联调时就是各种莫名其妙的问题。而这些问题通常又不会直接报“AES参数不对”这种错误而是以“解密出来乱码”“报错Malformed UTF-8”“后端解出来是空的”这类千奇百怪的形态出现。所以我强烈建议所有要接入AES的团队在上手写代码之前先花十分钟做一件事写一段AES联调参数文档把上面那张四要素对照表发给后端同学让后端直接按照这组参数出一个最简单的加解密接口前端同事也在本地用同样的明文跑通一个最小demo。双方先用自己的语言各自验证同一段“明文→密文→明文”的闭环然后再接入真实业务接口。这一步做到位后面能省下大量互相排查的时间。另外前端加解密代码写完后一定要经过code review。重点不是看逻辑对不对而是看密钥有没有被放到奇怪的地方、日志里有没有打印明文密码、是否出现把密钥提交进代码仓库的低级失误。工具类力求精简不要一个方法里又加密又处理业务参数又拼接报文后续维护会很痛苦。如果你项目的后端是由不同语言实现的解耦时也别忘了保持参数一致。Java、Node.js、Python、Go的库名和API完全不一样但底层标准是统一的。大家只要咬住“算法、模式、填充、编码”这八个字不放不管跨什么后端都能调通。AES加解密本质上是在“传输安全”这件事上多铺一层缓冲垫。把它做好了很多安全评审项能顺利通过把它做崩了各种数据错乱会让你怀疑人生。希望这篇文章能帮你少踩几个坑。尤其那个“密文解出来是一堆十六进制”的问题我摔过两次第二次还是在生产环境从那以后密文输出格式一定显式写死为Base64这个习惯建议你也养成。