Vue3使用CryptoJS实现AES加密解密实战教学
作者:菩提风
1. 前端数据安全:为什么我们需要加密?
在Web应用开发中,数据安全从来都不是可选项。我见过太多项目因为忽视基础加密而导致的敏感数据泄露案例。特别是在Vue这类前端框架中,当我们需要处理用户密码、身份令牌或敏感业务数据时,加密应该成为你的第一道防线。
CryptoJS作为JavaScript加密库的"瑞士军刀",提供了从基础哈希到高级加密的全套解决方案。不同于简单的Base64编码(这根本不是加密),AES这类对称加密算法能真正保护你的数据安全。我在金融项目中曾用AES-256加密交易指令,有效防止了中间人攻击。
2. 加密基础:理解核心概念
2.1 三种数据处理方式的本质区别
在开始写代码前,我们需要厘清几个关键概念:
Base64 :只是编码转换(类似把中文翻译成摩斯电码)
- 场景:图片内联、简单数据混淆
- 示例:
window.btoa('hello')→ "aGVsbG8=" - 警告:用Chrome控制台就能轻松解码,毫无安全性
MD5 :单向哈希函数(像榨汁机,无法还原)
- 场景:文件校验、旧式密码存储(已不推荐)
- 特点:固定输出32位十六进制
- 漏洞:彩虹表可破解,必须加盐
AES :真正的对称加密(带密钥的保险箱)
- 军事级安全标准
- 需要相同密钥加解密
- 支持128/192/256位密钥长度
重要提示:在2023年的密码学实践中,MD5已被完全弃用。对于新项目,至少应该使用SHA-256等更安全的哈希算法。
2.2 AES加密的核心参数
当我们在Vue中使用AES时,这些参数决定安全性:
{
mode: CryptoJS.mode.ECB, // 加密模式
padding: CryptoJS.pad.Pkcs7, // 填充方案
iv: CryptoJS.enc.Hex.parse("0000000000000000") // 初始化向量(ECB模式不需要)
}
ECB vs CBC :
- ECB:简单但不安全(相同明文生成相同密文)
- CBC:需要IV(初始化向量),更推荐
PKCS7填充 :
- 当数据不是16字节倍数时自动填充
- 解密时会自动移除填充
3. 实战:在Vue3中集成CryptoJS
3.1 项目初始化与安装
首先确保你的Vue3项目使用Vite(现代前端构建工具的首选):
npm create vite@latest vue-crypto-demo --template vue cd vue-crypto-demo npm install crypto-js --save
3.2 基础加密组件实现
这是一个完整的加密解密组件示例:
<template>
<div class="crypto-demo">
<h2>AES加解密演示</h2>
<textarea
v-model="plaintext"
placeholder="输入敏感数据..."
rows="3"
></textarea>
<div class="action-buttons">
<button @click="handleEncrypt" :disabled="!plaintext.trim()">
AES加密
</button>
<button
@click="handleDecrypt"
:disabled="!ciphertext.trim()"
class="decrypt-btn"
>
解密
</button>
</div>
<div v-if="ciphertext" class="result-box">
<h3>加密结果 (Base64格式):</h3>
<pre>{{ ciphertext }}</pre>
<button @click="handleCopy(ciphertext)" class="copy-btn">
复制密文
</button>
</div>
<div v-if="decryptedText" class="result-box">
<h3>解密结果:</h3>
<p>{{ decryptedText }}</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import CryptoJS from 'crypto-js'
const plaintext = ref('')
const ciphertext = ref('')
const decryptedText = ref('')
const error = ref('')
// 在实际项目中,密钥应该从环境变量获取
const SECRET_KEY = 'my-32-character-ultra-secure-key-123'
const handleEncrypt = () => {
try {
error.value = ''
const encrypted = CryptoJS.AES.encrypt(
plaintext.value,
SECRET_KEY,
{
mode: CryptoJS.mode.CBC,
iv: CryptoJS.enc.Utf8.parse('16-byte-iv-vector')
}
)
ciphertext.value = encrypted.toString()
decryptedText.value = ''
} catch (err) {
error.value = `加密失败: ${err.message}`
console.error('[加密错误]', err)
}
}
const handleDecrypt = () => {
try {
error.value = ''
const decrypted = CryptoJS.AES.decrypt(
ciphertext.value,
SECRET_KEY,
{
mode: CryptoJS.mode.CBC,
iv: CryptoJS.enc.Utf8.parse('16-byte-iv-vector')
}
)
decryptedText.value = decrypted.toString(CryptoJS.enc.Utf8)
if (!decryptedText.value) {
throw new Error('解密结果为空,可能是密钥错误')
}
} catch (err) {
error.value = `解密失败: ${err.message}`
console.error('[解密错误]', err)
}
}
const handleCopy = async (text) => {
try {
await navigator.clipboard.writeText(text)
alert('已复制到剪贴板')
} catch (copyErr) {
console.error('复制失败:', copyErr)
}
}
</script>
<style scoped>
.crypto-demo {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: 'Segoe UI', sans-serif;
}
textarea {
width: 100%;
padding: 0.8rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
.action-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
button {
padding: 0.6rem 1.2rem;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #359469;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.decrypt-btn {
background-color: #647eff;
}
.decrypt-btn:hover {
background-color: #4d63d1;
}
.result-box {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
border-left: 4px solid #42b983;
}
.error-message {
color: #ff4d4f;
padding: 0.8rem;
background-color: #fff2f0;
border-radius: 4px;
border-left: 4px solid #ff4d4f;
}
.copy-btn {
background-color: #647eff;
margin-top: 0.5rem;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
background-color: #f0f0f0;
padding: 0.5rem;
border-radius: 3px;
}
</style>3.3 关键实现解析
密钥管理 :
- 示例中硬编码密钥仅用于演示
- 生产环境应该:
- 从环境变量读取(
import.meta.env.VITE_APP_KEY) - 或通过安全API动态获取
- 从环境变量读取(
CBC模式增强安全 :
{
mode: CryptoJS.mode.CBC,
iv: CryptoJS.enc.Utf8.parse('16-byte-iv-vector')
}
初始化向量(IV)应该是随机的
每次加密生成新IV并和密文一起存储
错误处理 :
- 捕获所有加解密异常
- 对用户显示友好提示
- 控制台记录详细错误
4. 进阶:封装企业级加密工具
4.1 创建加密工具类
在 src/utils/crypto.js 中:
import CryptoJS from 'crypto-js'
// 获取配置好的密钥(优先从环境变量获取)
const getSecretKey = () => {
return import.meta.env.VITE_APP_AES_KEY || 'fallback-secret-key-32-chars'
}
// 生成随机IV(用于CBC模式)
const generateIV = () => {
return CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex)
}
/**
* AES加密(CBC模式 + PKCS7填充)
* @param {string} plaintext - 待加密文本
* @param {string} [key] - 可选密钥,不传则使用默认
* @returns {Object} { iv: string, ciphertext: string }
*/
export const encrypt = (plaintext, key) => {
const secretKey = key || getSecretKey()
const iv = generateIV()
const encrypted = CryptoJS.AES.encrypt(
plaintext,
CryptoJS.enc.Utf8.parse(secretKey),
{
iv: CryptoJS.enc.Hex.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
return {
iv,
ciphertext: encrypted.toString()
}
}
/**
* AES解密
* @param {string} ciphertext - 密文
* @param {string} iv - 初始化向量
* @param {string} [key] - 可选密钥
* @returns {string} 解密后的明文
*/
export const decrypt = (ciphertext, iv, key) => {
const secretKey = key || getSecretKey()
try {
const decrypted = CryptoJS.AES.decrypt(
ciphertext,
CryptoJS.enc.Utf8.parse(secretKey),
{
iv: CryptoJS.enc.Hex.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
const result = decrypted.toString(CryptoJS.enc.Utf8)
if (!result) {
throw new Error('解密失败:结果为空,可能是密钥或IV错误')
}
return result
} catch (error) {
console.error('[解密错误]', error)
throw new Error('解密失败:请检查密钥和密文')
}
}
/**
* 生成安全的随机密钥
* @param {number} [length=32] - 密钥长度(16/24/32对应AES-128/192/256)
* @returns {string} 16进制格式密钥
*/
export const generateKey = (length = 32) => {
const validLengths = [16, 24, 32]
if (!validLengths.includes(length)) {
throw new Error('密钥长度必须是16(AES-128)、24(AES-192)或32(AES-256)')
}
return CryptoJS.lib.WordArray.random(length).toString(CryptoJS.enc.Hex)
}
4.2 在Vue组件中使用
<script setup>
import { ref } from 'vue'
import { encrypt, decrypt } from '@/utils/crypto'
const inputText = ref('')
const encryptedData = ref(null)
const decryptedText = ref('')
const loading = ref(false)
const handleEncrypt = async () => {
loading.value = true
try {
encryptedData.value = encrypt(inputText.value)
decryptedText.value = ''
} catch (error) {
console.error('加密失败:', error)
} finally {
loading.value = false
}
}
const handleDecrypt = async () => {
if (!encryptedData.value) return
loading.value = true
try {
decryptedText.value = decrypt(
encryptedData.value.ciphertext,
encryptedData.value.iv
)
} catch (error) {
console.error('解密失败:', error)
} finally {
loading.value = false
}
}
</script>5. 安全最佳实践与常见陷阱
5.1 密钥管理黄金法则
永远不要 :
- 在前端代码硬编码生产密钥
- 使用版本控制系统提交密钥
- 使用简单易猜的密钥(如"password123")
应该 :
- 使用环境变量存储密钥
- 定期轮换密钥
- 不同环境使用不同密钥
5.2 常见加密漏洞
ECB模式陷阱 :
- 相同明文 → 相同密文
- 攻击者可能通过模式识别破解
- 解决方案:总是使用CBC或其他安全模式
IV复用问题 :
// 错误示范:固定IV const iv = 'static-iv-value-123' // 正确做法:每次加密生成随机IV const iv = CryptoJS.lib.WordArray.random(128/8)
密钥长度不足 :
- AES-128:16字节
- AES-256:32字节
- 解决方案:使用
generateKey()工具方法
5.3 性能优化技巧
大文件加密 :
- 分块处理(每1MB为一个块)
- 使用Web Workers避免UI阻塞
减少加密负载 :
- 只加密真正敏感的数据
- 非敏感信息可用Base64简单编码
缓存密钥 :
let cachedKey = null
const getKey = () => {
if (!cachedKey) {
cachedKey = CryptoJS.enc.Utf8.parse(getSecretKey())
}
return cachedKey
}
避免频繁解析密钥
6. 测试与调试指南
6.1 单元测试示例(使用Vitest)
import { describe, expect, it } from 'vitest'
import { encrypt, decrypt, generateKey } from '@/utils/crypto'
describe('AES加密套件', () => {
const testText = '这是一段测试明文123!@#'
it('正常加密解密流程', () => {
const { iv, ciphertext } = encrypt(testText)
const decrypted = decrypt(ciphertext, iv)
expect(decrypted).toBe(testText)
})
it('密钥不一致时解密失败', () => {
const { iv, ciphertext } = encrypt(testText, 'key1')
expect(() => decrypt(ciphertext, iv, 'key2')).toThrow()
})
it('生成符合长度的密钥', () => {
expect(generateKey(16).length).toBe(32) // 16字节=32十六进制字符
expect(generateKey(32).length).toBe(64)
})
})
6.2 常见错误排查
解密得到空字符串 :
- 检查密钥是否一致
- 验证IV是否匹配加密时使用的
- 确认加密模式/填充方案一致
中文乱码问题 :
// 确保加密前和解密后都使用UTF8编码 CryptoJS.enc.Utf8.parse(text) bytes.toString(CryptoJS.enc.Utf8)
跨平台兼容性 :
- 与其他语言(如Java/Python)交互时
- 确认双方使用相同的:
- 密钥编码方式
- IV生成逻辑
- 填充标准(PKCS5/PKCS7)
7. 扩展应用场景
7.1 本地存储加密
// 加密localStorage数据
const saveSecureData = (key, value) => {
const encrypted = encrypt(value)
localStorage.setItem(key, JSON.stringify(encrypted))
}
// 解密读取
const getSecureData = (key) => {
const data = localStorage.getItem(key)
if (!data) return null
try {
const { iv, ciphertext } = JSON.parse(data)
return decrypt(ciphertext, iv)
} catch (error) {
console.error('解密localStorage数据失败', error)
return null
}
}
7.2 接口数据保护
// 请求拦截器示例
apiClient.interceptors.request.use(config => {
if (config.encrypt) {
const { iv, ciphertext } = encrypt(JSON.stringify(config.data))
config.data = { iv, data: ciphertext }
config.headers['X-Encrypted'] = 'true'
}
return config
})
// 响应拦截器
apiClient.interceptors.response.use(response => {
if (response.config.decrypt && response.data?.iv) {
response.data = JSON.parse(
decrypt(response.data.data, response.data.iv)
)
}
return response
})
7.3 密码学哈希进阶
虽然本文聚焦AES,但对于密码哈希推荐:
// PBKDF2密钥派生(用于密码存储)
const hashPassword = (password, salt) => {
const iterations = 10000
const keySize = 256 / 32
return CryptoJS.PBKDF2(password, salt, {
keySize,
iterations,
hasher: CryptoJS.algo.SHA256
}).toString()
}
// 生成盐值
const generateSalt = () => {
return CryptoJS.lib.WordArray.random(128/8).toString()
}
8. 安全升级路线
从CryptoJS迁移到Web Crypto API :
- 浏览器原生支持
- 更优性能
- 需要处理兼容性
实现非对称加密 :
- 前端使用公钥加密
- 后端私钥解密
- 适合更高安全需求场景
动态密钥协商 :
- 基于DH密钥交换
- 每次会话生成临时密钥
- 彻底避免密钥硬编码问题
在多年的前端安全实践中,我深刻体会到加密不是"有没有"的问题,而是"做得好不好"的问题。一个常见的误区是开发者因为害怕复杂而选择简单的Base64或MD5,这实际上比不加密更危险——它制造了安全的假象。真正的加密该像这个AES实现一样,即使面对专业攻击也能提供可靠保护。
以上就是Vue3使用CryptoJS实现AES加密解密实战教学的详细内容,更多关于Vue3 CryptoJS加密的资料请关注脚本之家其它相关文章!
