vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3 CryptoJS加密

Vue3使用CryptoJS实现AES加密解密实战教学

作者:菩提风

CryptoJS作为JavaScript加密库的瑞士军刀,提供了从基础哈希到高级加密的全套解决方案,本文全面介绍了Vue3如何使用CryptoJS实现AES加密解密,感兴趣的小伙伴可以了解下

1. 前端数据安全:为什么我们需要加密?

在Web应用开发中,数据安全从来都不是可选项。我见过太多项目因为忽视基础加密而导致的敏感数据泄露案例。特别是在Vue这类前端框架中,当我们需要处理用户密码、身份令牌或敏感业务数据时,加密应该成为你的第一道防线。

CryptoJS作为JavaScript加密库的"瑞士军刀",提供了从基础哈希到高级加密的全套解决方案。不同于简单的Base64编码(这根本不是加密),AES这类对称加密算法能真正保护你的数据安全。我在金融项目中曾用AES-256加密交易指令,有效防止了中间人攻击。

2. 加密基础:理解核心概念

2.1 三种数据处理方式的本质区别

在开始写代码前,我们需要厘清几个关键概念:

Base64 :只是编码转换(类似把中文翻译成摩斯电码)

MD5 :单向哈希函数(像榨汁机,无法还原)

AES :真正的对称加密(带密钥的保险箱)

重要提示:在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 :

PKCS7填充 :

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 关键实现解析

密钥管理 :

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 密钥管理黄金法则

永远不要 :

应该 :

5.2 常见加密漏洞

ECB模式陷阱 :

IV复用问题 :

// 错误示范:固定IV
const iv = 'static-iv-value-123'

// 正确做法:每次加密生成随机IV
const iv = CryptoJS.lib.WordArray.random(128/8)

密钥长度不足 :

5.3 性能优化技巧

大文件加密 :

减少加密负载 :

缓存密钥 :

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 常见错误排查

解密得到空字符串 :

中文乱码问题 :

// 确保加密前和解密后都使用UTF8编码
CryptoJS.enc.Utf8.parse(text)
bytes.toString(CryptoJS.enc.Utf8)

跨平台兼容性 :

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 :

实现非对称加密 :

动态密钥协商 :

在多年的前端安全实践中,我深刻体会到加密不是"有没有"的问题,而是"做得好不好"的问题。一个常见的误区是开发者因为害怕复杂而选择简单的Base64或MD5,这实际上比不加密更危险——它制造了安全的假象。真正的加密该像这个AES实现一样,即使面对专业攻击也能提供可靠保护。

以上就是Vue3使用CryptoJS实现AES加密解密实战教学的详细内容,更多关于Vue3 CryptoJS加密的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文