CodeL
以前端为翼,以 AI 为脑,向全栈而行
2026-04-01

AES 加密实战教程

AES 加密实战教程:Vue 3 项目数据安全全攻略 从零掌握 AES 加密算法,4 个实战场景 + 完整 Vue 3 代码,搞定密码加密、敏感数据、本地存储安全! 一、核心概念 1.1 什么是 AES 加密? AES(...

AES 加密实战教程:Vue 3 项目数据安全全攻略 #

从零掌握 AES 加密算法,4 个实战场景 + 完整 Vue 3 代码,搞定密码加密、敏感数据、本地存储安全!


一、核心概念 #

1.1 什么是 AES 加密? #

AES(Advanced Encryption Standard) 是一种对称加密算法,就像一个"带锁的箱子":

  • 🔐 加密:把数据放进箱子,用钥匙锁上
  • 🔓 解密:用同一把钥匙打开箱子,取出数据

对称加密的意思是:加密和解密用同一把钥匙(密钥)。这和 RSA 等非对称加密不同(RSA 有两把钥匙:公钥加密,私钥解密)。

生活类比:

你有一个日记本,想防止别人偷看:
- AES 加密 = 把日记本锁进保险箱
- 密钥 = 保险箱的钥匙
- 加密过程 = 锁上保险箱
- 解密过程 = 打开保险箱看日记

1.2 为什么前端需要 AES 加密? #

场景 问题 AES 解决方案
用户密码传输 明文传输容易被截获 登录前加密,服务器解密验证
敏感数据存储 localStorage 明文存储不安全 加密后存储,取出时解密
API 数据传输 接口数据被抓包窃取 加密请求/响应数据
隐私信息保护 手机号、身份证等敏感信息 加密显示,点击查看才解密

⚠️ 注意:前端加密不能替代 HTTPS!

  • HTTPS 防止传输层被截获
  • AES 防止数据本身泄露
  • 两者结合才是最佳安全方案

1.3 AES 核心名词速查 #

名词 解释 类比
密钥(Key) 加密/解密的"钥匙" 保险箱钥匙
明文(Plaintext) 原始未加密数据 日记内容
密文(Ciphertext) 加密后的乱码数据 锁在保险箱里的日记
加密模式(Mode) AES 的工作方式 保险箱的锁类型
填充(Padding) 补齐数据长度 把日记垫满箱子
IV(初始向量) 随机数,增强安全性 每次换不同的锁
盐值(Salt) 防止相同密码产生相同密文 给钥匙加独特的纹理

1.4 AES 加密模式对比 #

模式 安全性 特点 适用场景
ECB ⚠️ 低 不需要 IV,相同明文→相同密文 不推荐
CBC ✅ 高 需要 IV,相同明文→不同密文 文件加密、数据传输
CFB ✅ 高 流式加密,错误不扩散 流数据加密
OFB ✅ 高 流式加密,预生成密钥流 实时通信
CTR ✅ 高 计数器模式,可并行处理 高性能场景
GCM ✅✅ 最高 认证加密,防篡改 推荐,API 通信

一句话总结:前端加密首选 CBC 或 GCM 模式!


二、基础使用 #

2.1 安装 crypto-js #

crypto-js 是最流行的 JavaScript 加密库,支持 AES、MD5、SHA 等多种算法。

# npm 安装
npm install crypto-js
 
# yarn 安装
yarn add crypto-js
 
# pnpm 安装
pnpm add crypto-js

2.2 最简单的 AES 加密 #

import AES from 'crypto-js/aes'
import CryptoJS from 'crypto-js'
 
// 密钥(实际项目中应该更复杂)
const key = 'my-secret-key-123'
 
// 要加密的数据
const plaintext = '这是要加密的敏感数据'
 
// 加密
const ciphertext = AES.encrypt(plaintext, key).toString()
console.log('加密结果:', ciphertext)
// 输出: "U2FsdGVkX1+xxx..."(Base64 编码的密文)
 
// 解密
const decrypted = AES.decrypt(ciphertext, key)
const originalText = decrypted.toString(CryptoJS.enc.Utf8)
console.log('解密结果:', originalText)
// 输出: "这是要加密的敏感数据"

2.3 使用 CBC 模式 + IV(推荐) #

import AES from 'crypto-js/aes'
import CryptoJS from 'crypto-js'
 
// 密钥(必须是 16/24/32 字节 = 128/192/256 位)
const key = CryptoJS.enc.Utf8.parse('1234567890123456') // 16 字节密钥
 
// IV(初始向量,16 字节,每次加密应该随机生成)
const iv = CryptoJS.enc.Utf8.parse('1234567890123456')
 
// 要加密的数据
const plaintext = 'Hello AES!'
 
// CBC 模式加密
const encrypted = AES.encrypt(plaintext, key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})
 
console.log('加密结果:', encrypted.toString())
 
// CBC 模式解密
const decrypted = AES.decrypt(encrypted.toString(), key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})
 
const originalText = decrypted.toString(CryptoJS.enc.Utf8)
console.log('解密结果:', originalText)

2.4 密钥格式转换 #

crypto-js 支持多种密钥格式:

import CryptoJS from 'crypto-js'
 
// 字符串密钥 → WordArray
const keyFromString = CryptoJS.enc.Utf8.parse('my-key-12345678') // 16字节
 
// Base64 密钥 → WordArray
const keyFromBase64 = CryptoJS.enc.Base64.parse('bXkta2V5LTEyMzQ1Njc4')
 
// Hex 密钥 → WordArray
const keyFromHex = CryptoJS.enc.Hex.parse('6d792d6b65792d3132333435363738')
 
// 生成随机密钥(推荐)
const randomKey = CryptoJS.lib.WordArray.random(16) // 16 字节 = 128 位
 
// 生成随机 IV
const randomIV = CryptoJS.lib.WordArray.random(16)
 
// WordArray → 各格式输出
const keyBytes = CryptoJS.enc.Utf8.parse('1234567890123456')
console.log('Utf8:', keyBytes.toString())
console.log('Base64:', keyBytes.toString(CryptoJS.enc.Base64))
console.log('Hex:', keyBytes.toString(CryptoJS.enc.Hex))

三、进阶用法 #

3.1 密钥管理最佳实践 #

❌ 错误做法:硬编码密钥

// 不要这样写!密钥泄露风险
const key = 'hardcoded-secret-key'

✅ 正确做法:动态生成 + 安全存储

// 1. 从环境变量读取
const key = process.env.VUE_APP_AES_KEY
 
// 2. 用户登录后动态生成
function generateUserKey(userId, password) {
  // 使用用户 ID + 密码生成唯一密钥
  const salt = CryptoJS.enc.Utf8.parse(userId)
  const key256 = CryptoJS.PBKDF2(password, salt, {
    keySize: 256 / 32, // 256 位
    iterations: 1000
  })
  return key256
}
 
// 3. 随机生成并存储到服务器
const sessionKey = CryptoJS.lib.WordArray.random(32) // 256 位
// 发送 sessionKey 到服务器,服务器返回加密的 sessionKey

3.2 PBKDF2 密钥派生 #

PBKDF2 可以从用户密码生成安全的加密密钥:

import CryptoJS from 'crypto-js'
 
/**
 * 从密码派生密钥
 * @param {string} password - 用户密码
 * @param {string} salt - 盐值(可以是用户 ID)
 * @param {number} keySize - 密钥长度(字节)
 * @param {number} iterations - 迭代次数(越多越安全,但越慢)
 */
function deriveKey(password, salt, keySize = 32, iterations = 10000) {
  const saltBytes = CryptoJS.enc.Utf8.parse(salt)
  
  const key = CryptoJS.PBKDF2(password, saltBytes, {
    keySize: keySize / 4, // WordArray 单位是 4 字节
    iterations: iterations,
    hasher: CryptoJS.algo.SHA256
  })
  
  return key
}
 
// 使用示例
const password = 'user-password-123'
const salt = 'user-id-456'
const derivedKey = deriveKey(password, salt, 32) // 256 位密钥
 
console.log('派生密钥:', derivedKey.toString(CryptoJS.enc.Hex))

3.3 GCM 认证加密(最高安全) #

GCM 模式提供加密 + 认证,防止数据被篡改:

import AES from 'crypto-js/aes'
import CryptoJS from 'crypto-js'
 
/**
 * AES-GCM 加密(需要 crypto-js 4.1.1+)
 * 注意:crypto-js 的 GCM 实现有限,推荐使用 Web Crypto API
 */
function encryptGCM(plaintext, key, iv) {
  const keyBytes = CryptoJS.enc.Utf8.parse(key)
  const ivBytes = CryptoJS.enc.Utf8.parse(iv)
  
  const encrypted = AES.encrypt(plaintext, keyBytes, {
    iv: ivBytes,
    mode: CryptoJS.mode.CTR, // crypto-js 不原生支持 GCM,用 CTR 替代
    padding: CryptoJS.pad.NoPadding
  })
  
  return encrypted.toString()
}

推荐:使用 Web Crypto API(浏览器原生支持)

/**
 * Web Crypto API AES-GCM 加密(浏览器原生,性能更好)
 */
async function webCryptoEncrypt(plaintext, keyString) {
  // 生成密钥
  const keyData = new TextEncoder().encode(keyString)
  const key = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'AES-GCM' },
    false,
    ['encrypt', 'decrypt']
  )
  
  // 生成随机 IV(GCM 推荐 12 字节)
  const iv = crypto.getRandomValues(new Uint8Array(12))
  
  // 加密
  const data = new TextEncoder().encode(plaintext)
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    data
  )
  
  // 返回 IV + 密文(IV 需要保存用于解密)
  return {
    iv: Array.from(iv),
    ciphertext: Array.from(new Uint8Array(encrypted))
  }
}
 
/**
 * Web Crypto API AES-GCM 解密
 */
async function webCryptoDecrypt(ciphertext, iv, keyString) {
  // 生成密钥
  const keyData = new TextEncoder().encode(keyString)
  const key = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'AES-GCM' },
    false,
    ['encrypt', 'decrypt']
  )
  
  // 解密
  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(iv) },
    key,
    new Uint8Array(ciphertext)
  )
  
  return new TextDecoder().decode(decrypted)
}
 
// 使用示例
async function example() {
  const key = '1234567890123456' // 16 字节
  const plaintext = 'Hello Web Crypto!'
  
  const { iv, ciphertext } = await webCryptoEncrypt(plaintext, key)
  console.log('加密结果:', { iv, ciphertext })
  
  const decrypted = await webCryptoDecrypt(ciphertext, iv, key)
  console.log('解密结果:', decrypted)
}

3.4 加密数据完整性验证 #

加密后添加 HMAC 签名,防止密文被篡改:

import CryptoJS from 'crypto-js'
 
/**
 * 加密 + HMAC 签名
 */
function encryptWithHMAC(plaintext, aesKey, hmacKey) {
  // AES 加密
  const encrypted = AES.encrypt(plaintext, aesKey).toString()
  
  // HMAC 签名(防止篡改)
  const hmac = CryptoJS.HmacSHA256(encrypted, hmacKey).toString()
  
  return {
    ciphertext: encrypted,
    hmac: hmac
  }
}
 
/**
 * 验证 HMAC + 解密
 */
function decryptWithHMAC(data, aesKey, hmacKey) {
  // 验证 HMAC
  const expectedHmac = CryptoJS.HmacSHA256(data.ciphertext, hmacKey).toString()
  
  if (expectedHmac !== data.hmac) {
    throw new Error('数据被篡改,拒绝解密!')
  }
  
  // 解密
  const decrypted = AES.decrypt(data.ciphertext, aesKey)
  return decrypted.toString(CryptoJS.enc.Utf8)
}
 
// 使用示例
const aesKey = 'aes-key-12345678'
const hmacKey = 'hmac-key-12345678'
const plaintext = '重要数据'
 
const encryptedData = encryptWithHMAC(plaintext, aesKey, hmacKey)
console.log('加密 + 签名:', encryptedData)
 
const decrypted = decryptWithHMAC(encryptedData, aesKey, hmacKey)
console.log('验证 + 解密:', decrypted)

四、实战场景 #

场景 1:Vue 3 AES 加密 Composable #

创建可复用的组合式函数:

// composables/useAES.ts
import { ref, computed } from 'vue'
import AES from 'crypto-js/aes'
import CryptoJS from 'crypto-js'
 
export interface AESOptions {
  key?: string
  iv?: string
  mode?: CryptoJS.mode.Mode
  padding?: CryptoJS.pad.Padding
}
 
export function useAES(options: AESOptions = {}) {
  // 默认配置
  const key = ref(options.key || '')
  const iv = ref(options.iv || '')
  const mode = options.mode || CryptoJS.mode.CBC
  const padding = options.padding || CryptoJS.pad.Pkcs7
  
  // 错误状态
  const error = ref<string | null>(null)
  
  // 是否已配置密钥
  const isReady = computed(() => key.value.length >= 16)
  
  /**
   * 设置密钥
   */
  const setKey = (newKey: string) => {
    if (newKey.length < 16) {
      error.value = '密钥长度必须至少 16 字节'
      return false
    }
    key.value = newKey
    error.value = null
    return true
  }
  
  /**
   * 设置 IV
   */
  const setIV = (newIV: string) => {
    if (newIV.length < 16) {
      error.value = 'IV 长度必须至少 16 字节'
      return false
    }
    iv.value = newIV
    error.value = null
    return true
  }
  
  /**
   * 生成随机密钥
   */
  const generateKey = (length: number = 32) => {
    const randomKey = CryptoJS.lib.WordArray.random(length)
    key.value = randomKey.toString(CryptoJS.enc.Utf8) || 
                randomKey.toString(CryptoJS.enc.Base64)
    return key.value
  }
  
  /**
   * 生成随机 IV
   */
  const generateIV = () => {
    const randomIV = CryptoJS.lib.WordArray.random(16)
    iv.value = randomIV.toString(CryptoJS.enc.Utf8) ||
               randomIV.toString(CryptoJS.enc.Base64)
    return iv.value
  }
  
  /**
   * 加密字符串
   */
  const encrypt = (plaintext: string): string | null => {
    if (!isReady.value) {
      error.value = '密钥未配置或长度不足'
      return null
    }
    
    try {
      const keyBytes = CryptoJS.enc.Utf8.parse(key.value)
      const ivBytes = iv.value ? CryptoJS.enc.Utf8.parse(iv.value) : undefined
      
      const encrypted = AES.encrypt(plaintext, keyBytes, {
        iv: ivBytes,
        mode: mode,
        padding: padding
      })
      
      error.value = null
      return encrypted.toString()
    } catch (e) {
      error.value = `加密失败: ${e.message}`
      return null
    }
  }
  
  /**
   * 解密字符串
   */
  const decrypt = (ciphertext: string): string | null => {
    if (!isReady.value) {
      error.value = '密钥未配置或长度不足'
      return null
    }
    
    try {
      const keyBytes = CryptoJS.enc.Utf8.parse(key.value)
      const ivBytes = iv.value ? CryptoJS.enc.Utf8.parse(iv.value) : undefined
      
      const decrypted = AES.decrypt(ciphertext, keyBytes, {
        iv: ivBytes,
        mode: mode,
        padding: padding
      })
      
      const originalText = decrypted.toString(CryptoJS.enc.Utf8)
      
      if (!originalText) {
        error.value = '解密失败:密钥错误或数据损坏'
        return null
      }
      
      error.value = null
      return originalText
    } catch (e) {
      error.value = `解密失败: ${e.message}`
      return null
    }
  }
  
  /**
   * 加密对象(JSON)
   */
  const encryptObject = <T>(obj: T): string | null => {
    const jsonString = JSON.stringify(obj)
    return encrypt(jsonString)
  }
  
  /**
   * 解密对象(JSON)
   */
  const decryptObject = <T>(ciphertext: string): T | null => {
    const jsonString = decrypt(ciphertext)
    if (!jsonString) return null
    
    try {
      return JSON.parse(jsonString) as T
    } catch (e) {
      error.value = `JSON 解析失败: ${e.message}`
      return null
    }
  }
  
  /**
   * 加密文件内容(Base64)
   */
  const encryptBase64 = (base64Data: string): string | null => {
    if (!isReady.value) {
      error.value = '密钥未配置'
      return null
    }
    
    try {
      const keyBytes = CryptoJS.enc.Utf8.parse(key.value)
      const ivBytes = iv.value ? CryptoJS.enc.Utf8.parse(iv.value) : undefined
      
      // Base64 → WordArray
      const dataBytes = CryptoJS.enc.Base64.parse(base64Data)
      
      const encrypted = AES.encrypt(dataBytes, keyBytes, {
        iv: ivBytes,
        mode: mode,
        padding: padding
      })
      
      return encrypted.toString()
    } catch (e) {
      error.value = `加密失败: ${e.message}`
      return null
    }
  }
  
  /**
   * 解密到 Base64
   */
  const decryptToBase64 = (ciphertext: string): string | null => {
    if (!isReady.value) {
      error.value = '密钥未配置'
      return null
    }
    
    try {
      const keyBytes = CryptoJS.enc.Utf8.parse(key.value)
      const ivBytes = iv.value ? CryptoJS.enc.Utf8.parse(iv.value) : undefined
      
      const decrypted = AES.decrypt(ciphertext, keyBytes, {
        iv: ivBytes,
        mode: mode,
        padding: padding
      })
      
      return decrypted.toString(CryptoJS.enc.Base64)
    } catch (e) {
      error.value = `解密失败: ${e.message}`
      return null
    }
  }
  
  return {
    // 状态
    key,
    iv,
    error,
    isReady,
    
    // 配置
    setKey,
    setIV,
    generateKey,
    generateIV,
    
    // 操作
    encrypt,
    decrypt,
    encryptObject,
    decryptObject,
    encryptBase64,
    decryptToBase64
  }
}

场景 2:用户密码加密传输 #

登录/注册时加密密码,防止明文传输:

<!-- components/LoginForm.vue -->
<template>
  <form @submit.prevent="handleLogin" class="login-form">
    <div class="form-group">
      <label>用户名</label>
      <input 
        v-model="username" 
        type="text" 
        placeholder="请输入用户名"
        required
      />
    </div>
    
    <div class="form-group">
      <label>密码</label>
      <input 
        v-model="password" 
        type="password" 
        placeholder="请输入密码"
        required
      />
    </div>
    
    <div class="form-actions">
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </div>
    
    <div v-if="error" class="error-message">
      {{ error }}
    </div>
  </form>
</template>
 
<script setup lang="ts">
import { ref } from 'vue'
import { useAES } from '@/composables/useAES'
import CryptoJS from 'crypto-js'
 
// 状态
const username = ref('')
const password = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
 
// AES 加密(从环境变量获取密钥)
const aesKey = import.meta.env.VITE_APP_AES_KEY || 'default-key-12345'
const { encrypt, generateIV, setKey } = useAES()
setKey(aesKey)
 
/**
 * 处理登录
 */
async function handleLogin() {
  loading.value = true
  error.value = null
  
  try {
    // 1. 生成随机 IV(每次登录不同)
    const iv = generateIV()
    
    // 2. 加密密码
    const encryptedPassword = encrypt(password.value)
    
    if (!encryptedPassword) {
      throw new Error('密码加密失败')
    }
    
    // 3. 发送加密后的登录请求
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: username.value,
        password: encryptedPassword, // 加密后的密码
        iv: iv, // IV 需要发送给服务器解密
        timestamp: Date.now() // 防止重放攻击
      })
    })
    
    const data = await response.json()
    
    if (data.success) {
      // 登录成功
      console.log('登录成功:', data.user)
      // 保存 token 等...
    } else {
      error.value = data.message || '登录失败'
    }
  } catch (e) {
    error.value = e.message || '网络错误,请稍后重试'
  } finally {
    loading.value = false
  }
}
 
/**
 * PBKDF2 增强密码加密(可选)
 */
function encryptPasswordWithPBKDF2(password: string, userId: string) {
  // 使用 PBKDF2 从密码派生密钥
  const salt = CryptoJS.enc.Utf8.parse(userId + Date.now())
  const derivedKey = CryptoJS.PBKDF2(password, salt, {
    keySize: 256 / 32,
    iterations: 10000,
    hasher: CryptoJS.algo.SHA256
  })
  
  // 用派生密钥加密密码
  const encrypted = AES.encrypt(password, derivedKey, {
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  })
  
  return {
    ciphertext: encrypted.toString(),
    salt: salt.toString(CryptoJS.enc.Base64)
  }
}
</script>
 
<style scoped>
.login-form {
  max-width: 400px;
  padding: 20px;
}
 
.form-group {
  margin-bottom: 16px;
}
 
.form-group label {
  display: block;
  margin-bottom: 4px;
}
 
.form-group input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
 
.form-actions button {
  width: 100%;
  padding: 12px;
  background: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
 
.form-actions button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
 
.error-message {
  margin-top: 16px;
  padding: 10px;
  background: #ffebee;
  color: #f44336;
  border-radius: 4px;
}
</style>

Node.js 服务器端解密示例:

// server.js
const CryptoJS = require('crypto-js')
const AES = require('crypto-js/aes')
 
/**
 * 登录接口
 */
app.post('/api/login', async (req, res) => {
  const { username, password, iv, timestamp } = req.body
  
  // 检查时间戳(防止重放攻击)
  const now = Date.now()
  if (now - timestamp > 60000) { // 60 秒内有效
    return res.json({ success: false, message: '请求过期' })
  }
  
  // 解密密码
  const aesKey = process.env.AES_KEY // 与前端相同的密钥
  const ivBytes = CryptoJS.enc.Utf8.parse(iv)
  const keyBytes = CryptoJS.enc.Utf8.parse(aesKey)
  
  const decrypted = AES.decrypt(password, keyBytes, {
    iv: ivBytes,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  })
  
  const originalPassword = decrypted.toString(CryptoJS.enc.Utf8)
  
  // 验证用户名和密码
  const user = await db.findUser(username)
  
  if (user && user.password === hashPassword(originalPassword)) {
    res.json({
      success: true,
      user: { id: user.id, name: user.name },
      token: generateToken(user)
    })
  } else {
    res.json({ success: false, message: '用户名或密码错误' })
  }
})

场景 3:敏感数据本地存储加密 #

加密 localStorage/sessionStorage 数据:

// utils/secureStorage.ts
import { useAES } from '@/composables/useAES'
import CryptoJS from 'crypto-js'
 
/**
 * 安全存储类(加密 localStorage)
 */
export class SecureStorage {
  private aes: ReturnType<typeof useAES>
  private storage: Storage
  
  constructor(storage: Storage = localStorage, key?: string) {
    this.storage = storage
    this.aes = useAES()
    
    // 设置密钥(从环境变量或参数)
    const aesKey = key || import.meta.env.VITE_APP_AES_KEY
    if (aesKey) {
      this.aes.setKey(aesKey)
    }
  }
  
  /**
   * 加密存储
   */
  set<T>(key: string, value: T): boolean {
    try {
      // 转为 JSON
      const jsonValue = JSON.stringify(value)
      
      // 加密
      const encrypted = this.aes.encrypt(jsonValue)
      
      if (!encrypted) {
        console.error('加密失败')
        return false
      }
      
      // 存储(添加完整性校验)
      const hmac = CryptoJS.HmacSHA256(encrypted, this.aes.key.value).toString()
      
      this.storage.setItem(key, JSON.stringify({
        ciphertext: encrypted,
        hmac: hmac,
        timestamp: Date.now()
      }))
      
      return true
    } catch (e) {
      console.error('存储失败:', e)
      return false
    }
  }
  
  /**
   * 解密读取
   */
  get<T>(key: string): T | null {
    try {
      const stored = this.storage.getItem(key)
      
      if (!stored) return null
      
      const data = JSON.parse(stored)
      
      // 验证完整性
      const expectedHmac = CryptoJS.HmacSHA256(
        data.ciphertext, 
        this.aes.key.value
      ).toString()
      
      if (expectedHmac !== data.hmac) {
        console.error('数据完整性校验失败,可能被篡改')
        this.storage.removeItem(key) // 删除被篡改的数据
        return null
      }
      
      // 解密
      const decrypted = this.aes.decrypt(data.ciphertext)
      
      if (!decrypted) return null
      
      return JSON.parse(decrypted) as T
    } catch (e) {
      console.error('读取失败:', e)
      return null
    }
  }
  
  /**
   * 删除
   */
  remove(key: string): void {
    this.storage.removeItem(key)
  }
  
  /**
   * 清空
   */
  clear(): void {
    this.storage.clear()
  }
  
  /**
   * 获取所有加密的 key
   */
  keys(): string[] {
    const keys: string[] = []
    for (let i = 0; i < this.storage.length; i++) {
      keys.push(this.storage.key(i) || '')
    }
    return keys
  }
}
 
// 创建实例
export const secureLocalStorage = new SecureStorage(localStorage)
export const secureSessionStorage = new SecureStorage(sessionStorage)
 
// 使用示例
interface UserSession {
  userId: string
  token: string
  role: string
  expiresAt: number
}
 
// 存储
secureLocalStorage.set<UserSession>('userSession', {
  userId: '123',
  token: 'abc-def-ghi',
  role: 'admin',
  expiresAt: Date.now() + 3600000
})
 
// 读取
const session = secureLocalStorage.get<UserSession>('userSession')
console.log('用户会话:', session)
 
// 删除
secureLocalStorage.remove('userSession')

Vue 3 组件使用示例:

<!-- components/UserProfile.vue -->
<template>
  <div class="user-profile">
    <h2>用户信息</h2>
    
    <!-- 敏感信息加密显示 -->
    <div class="info-item">
      <label>手机号:</label>
      <span v-if="!showPhone">{{ maskedPhone }}</span>
      <span v-else>{{ decryptedPhone }}</span>
      <button @click="togglePhone" class="toggle-btn">
        {{ showPhone ? '隐藏' : '查看' }}
      </button>
    </div>
    
    <div class="info-item">
      <label>身份证:</label>
      <span v-if="!showIdCard">{{ maskedIdCard }}</span>
      <span v-else>{{ decryptedIdCard }}</span>
      <button @click="toggleIdCard" class="toggle-btn">
        {{ showIdCard ? '隐藏' : '查看' }}
      </button>
    </div>
    
    <!-- 加密存储用户数据 -->
    <button @click="saveUserData" class="save-btn">
      安全保存数据
    </button>
    
    <button @click="loadUserData" class="load-btn">
      加载保存的数据
    </button>
  </div>
</template>
 
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useAES } from '@/composables/useAES'
import { secureLocalStorage } from '@/utils/secureStorage'
 
// 状态
const showPhone = ref(false)
const showIdCard = ref(false)
 
// 加密的原始数据(从服务器获取)
const encryptedPhone = ref('') // 加密的手机号
const encryptedIdCard = ref('') // 加密的身份证
 
// AES
const { decrypt } = useAES()
const aesKey = import.meta.env.VITE_APP_AES_KEY || 'default-key-12345'
 
// 解密后的数据
const decryptedPhone = computed(() => {
  if (!encryptedPhone.value) return ''
  return decrypt(encryptedPhone.value) || ''
})
 
const decryptedIdCard = computed(() => {
  if (!encryptedIdCard.value) return ''
  return decrypt(encryptedIdCard.value) || ''
})
 
// 脱敏显示
const maskedPhone = computed(() => {
  if (!decryptedPhone.value) return ''
  // 显示中间4位,其余用 * 替代
  return decryptedPhone.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
})
 
const maskedIdCard = computed(() => {
  if (!decryptedIdCard.value) return ''
  // 显示前6位和后4位,其余用 * 替代
  return decryptedIdCard.value.replace(/(\d{6})\d{9}(\d{4})/, '$1*********$2')
})
 
// 切换显示
const togglePhone = () => {
  showPhone.value = !showPhone.value
}
 
const toggleIdCard = () => {
  showIdCard.value = !showIdCard.value
}
 
// 安全存储用户数据
const saveUserData = () => {
  const userData = {
    phone: decryptedPhone.value,
    idCard: decryptedIdCard.value,
    savedAt: Date.now()
  }
  
  secureLocalStorage.set('userData', userData)
  alert('数据已加密保存')
}
 
// 加载保存的数据
const loadUserData = () => {
  const userData = secureLocalStorage.get<{
    phone: string
    idCard: string
    savedAt: number
  }>('userData')
  
  if (userData) {
    console.log('加载的数据:', userData)
    alert(`数据加载成功,保存时间: ${new Date(userData.savedAt).toLocaleString()}`)
  } else {
    alert('未找到保存的数据')
  }
}
 
// 初始化:从服务器获取加密数据
onMounted(async () => {
  const response = await fetch('/api/user/sensitive-data')
  const data = await response.json()
  
  encryptedPhone.value = data.phone // 服务器返回的加密手机号
  encryptedIdCard.value = data.idCard // 服务器返回的加密身份证
})
</script>
 
<style scoped>
.user-profile {
  padding: 20px;
}
 
.info-item {
  display: flex;
  align-items: center;
  margin-bottom: 16px;
}
 
.info-item label {
  width: 100px;
}
 
.toggle-btn {
  margin-left: 16px;
  padding: 4px 12px;
  background: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
 
.save-btn, .load-btn {
  margin-right: 16px;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
 
.save-btn {
  background: #4caf50;
  color: white;
}
 
.load-btn {
  background: #ff9800;
  color: white;
}
</style>

场景 4:API 数据加密传输 #

加密 API 请求/响应数据:

// utils/apiEncryption.ts
import { useAES } from '@/composables/useAES'
import CryptoJS from 'crypto-js'
 
/**
 * 加密 API 客户端
 */
export class EncryptedAPI {
  private aes: ReturnType<typeof useAES>
  private baseUrl: string
  private hmacKey: string
  
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
    this.aes = useAES()
    
    // 设置密钥
    const aesKey = import.meta.env.VITE_APP_AES_KEY
    this.hmacKey = import.meta.env.VITE_APP_HMAC_KEY || aesKey
    
    if (aesKey) {
      this.aes.setKey(aesKey)
      this.aes.generateIV() // 每次请求生成新 IV
    }
  }
  
  /**
   * 加密请求
   */
  private encryptRequest(data: any) {
    const json = JSON.stringify(data)
    
    // 生成随机 IV
    const iv = this.aes.generateIV()
    
    // 加密数据
    const encrypted = this.aes.encrypt(json)
    
    if (!encrypted) {
      throw new Error('请求加密失败')
    }
    
    // HMAC 签名
    const hmac = CryptoJS.HmacSHA256(encrypted, this.hmacKey).toString()
    
    return {
      ciphertext: encrypted,
      iv: iv,
      hmac: hmac,
      timestamp: Date.now()
    }
  }
  
  /**
   * 解密响应
   */
  private decryptResponse(data: any) {
    // 验证 HMAC
    const expectedHmac = CryptoJS.HmacSHA256(
      data.ciphertext, 
      this.hmacKey
    ).toString()
    
    if (expectedHmac !== data.hmac) {
      throw new Error('响应数据完整性校验失败')
    }
    
    // 设置 IV
    this.aes.setIV(data.iv)
    
    // 解密
    const decrypted = this.aes.decrypt(data.ciphertext)
    
    if (!decrypted) {
      throw new Error('响应解密失败')
    }
    
    return JSON.parse(decrypted)
  }
  
  /**
   * 发送加密请求
   */
  async post(endpoint: string, data: any): Promise<any> {
    const encryptedData = this.encryptRequest(data)
    
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Encrypted': 'true' // 标记加密请求
      },
      body: JSON.stringify(encryptedData)
    })
    
    const responseData = await response.json()
    
    // 检查是否是加密响应
    if (responseData.ciphertext) {
      return this.decryptResponse(responseData)
    }
    
    return responseData
  }
  
  /**
   * 发送加密 GET 请求(query 参数加密)
   */
  async get(endpoint: string, params: Record<string, any> = {}): Promise<any> {
    // 加密 query 参数
    const encryptedParams = this.encryptRequest(params)
    
    // 将加密数据放在一个参数中
    const url = new URL(`${this.baseUrl}${endpoint}`)
    url.searchParams.set('encrypted', JSON.stringify(encryptedParams))
    
    const response = await fetch(url.toString())
    const responseData = await response.json()
    
    if (responseData.ciphertext) {
      return this.decryptResponse(responseData)
    }
    
    return responseData
  }
}
 
// 创建实例
export const encryptedAPI = new EncryptedAPI(
  import.meta.env.VITE_APP_API_BASE_URL || '/api'
)
 
// 使用示例
async function fetchUserData(userId: string) {
  const data = await encryptedAPI.post('/user/get', {
    userId,
    includeSensitive: true
  })
  
  console.log('用户数据:', data)
  return data
}

完整 Vue 3 服务封装:

// services/encryptedUserService.ts
import { encryptedAPI } from '@/utils/apiEncryption'
import { secureLocalStorage } from '@/utils/secureStorage'
 
export interface User {
  id: string
  name: string
  email: string
  phone?: string // 加密字段
  idCard?: string // 加密字段
  role: string
}
 
export class EncryptedUserService {
  /**
   * 登录(加密密码)
   */
  async login(username: string, password: string): Promise<User> {
    const result = await encryptedAPI.post('/auth/login', {
      username,
      password // 会自动加密
    })
    
    if (result.success) {
      // 安全存储会话
      secureLocalStorage.set('session', {
        user: result.user,
        token: result.token,
        expiresAt: Date.now() + 7 * 24 * 3600000 // 7天
      })
      
      return result.user
    }
    
    throw new Error(result.message || '登录失败')
  }
  
  /**
   * 获取用户信息(加密传输)
   */
  async getUser(userId: string): Promise<User> {
    return await encryptedAPI.post('/user/get', { userId })
  }
  
  /**
   * 更新用户信息(加密传输)
   */
  async updateUser(userId: string, data: Partial<User>): Promise<User> {
    return await encryptedAPI.post('/user/update', {
      userId,
      data
    })
  }
  
  /**
   * 获取敏感信息(加密传输 + 加密显示)
   */
  async getSensitiveInfo(userId: string): Promise<{
    phone: string
    idCard: string
  }> {
    return await encryptedAPI.post('/user/sensitive', { userId })
  }
  
  /**
   * 检查登录状态
   */
  isLoggedIn(): boolean {
    const session = secureLocalStorage.get<{
      user: User
      token: string
      expiresAt: number
    }>('session')
    
    if (!session) return false
    
    // 检查过期
    if (Date.now() > session.expiresAt) {
      secureLocalStorage.remove('session')
      return false
    }
    
    return true
  }
  
  /**
   * 获取当前用户
   */
  getCurrentUser(): User | null {
    const session = secureLocalStorage.get<{
      user: User
      token: string
      expiresAt: number
    }>('session')
    
    return session?.user || null
  }
  
  /**
   * 登出
   */
  logout(): void {
    secureLocalStorage.remove('session')
  }
}
 
// 导出服务实例
export const userService = new EncryptedUserService()

五、常见问题 #

Q1: AES 密钥长度有什么要求? #

AES 密钥长度决定加密强度:

密钥长度 AES 类型 安全性 性能
16 字节 (128 位) AES-128 ✅ 安全 最快
24 字节 (192 位) AES-192 ✅✅ 更安全 中等
32 字节 (256 位) AES-256 ✅✅✅ 最安全 较慢

最佳实践:

  • 前端推荐 AES-128(性能好,足够安全)
  • 高敏感数据推荐 AES-256
  • 密钥必须是固定长度,不能随意长度
// 正确:16/24/32 字节
const key128 = CryptoJS.enc.Utf8.parse('1234567890123456') // 16字节 ✅
const key192 = CryptoJS.enc.Utf8.parse('123456789012345678901234') // 24字节 ✅
const key256 = CryptoJS.enc.Utf8.parse('12345678901234567890123456789012') // 32字节 ✅
 
// 错误:长度不对
const wrongKey = CryptoJS.enc.Utf8.parse('abc') // 3字节 ❌

Q2: IV 是什么?为什么需要它? #

IV(Initial Vector) 是初始向量,用于增强加密安全性。

类比理解:

没有 IV:相同的明文 + 相同密钥 → 相同密文
有 IV:相同的明文 + 相同密钥 + 不同 IV → 不同密文
 
就像:
没有 IV = 每次用同样的锁锁箱子
有 IV = 每次换不同的锁锁箱子

为什么重要?

  • 防止密码分析攻击(通过对比相同密文找规律)
  • 防止重放攻击(即使密钥泄露,没有 IV 也无法解密)

最佳实践:

// 每次加密生成新的 IV
const iv = CryptoJS.lib.WordArray.random(16)
 
// IV 需要保存(解密时需要)
// IV 可以公开传输(不需要加密)

Q3: ECB 模式为什么不安全? #

ECB(Electronic Codebook) 模式的问题:

加密 "HelloHelloHello"(重复的 Hello)
ECB 结果:[密文A][密文A][密文A](重复的密文)
CBC 结果:[密文B][密文C][密文D](不同的密文)

问题:

  • 相同明文块 → 相同密文块
  • 可以分析出数据模式
  • 著名的"企鹅图"攻击案例

结论:永远不要使用 ECB 模式!

Q4: 前端加密后,后端怎么解密? #

前后端需要约定:

  1. 相同的密钥(或密钥协商机制)
  2. 相同的模式(CBC 推荐)
  3. 相同的填充(Pkcs7 推荐)
  4. IV 传输(前端生成,传给后端)
// 前端加密
const encrypted = AES.encrypt(plaintext, key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
}).toString()
 
// 发送给后端
fetch('/api', {
  method: 'POST',
  body: JSON.stringify({
    ciphertext: encrypted,
    iv: iv.toString() // IV 需要传输
  })
})
 
// 后端解密(Node.js)
const AES = require('crypto-js/aes')
const CryptoJS = require('crypto-js')
 
const decrypted = AES.decrypt(req.body.ciphertext, key, {
  iv: CryptoJS.enc.Utf8.parse(req.body.iv),
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})
 
const plaintext = decrypted.toString(CryptoJS.enc.Utf8)

Q5: 加密数据如何存储? #

推荐格式:

// 单一密文存储
localStorage.setItem('token', encryptedToken)
 
// 完整格式存储(推荐)
const encryptedData = {
  ciphertext: encrypted,    // 密文
  iv: iv.toString(),       // IV(Base64)
  hmac: hmac.toString(),   // HMAC 签名(防篡改)
  timestamp: Date.now()    // 时间戳(防过期)
}
 
localStorage.setItem('data', JSON.stringify(encryptedData))

为什么完整格式更好?

  • IV 可恢复(解密需要)
  • HMAC 可验证完整性
  • 时间戳可检查过期

Q6: 如何处理加密失败? #

常见错误:

错误 原因 解决方案
Malformed UTF-8 data 解密结果不是 UTF-8 密钥错误或数据损坏
encrypt failed 密钥长度不对 确保 16/24/32 字节
null/undefined 解密失败 检查密钥、IV、模式匹配

错误处理代码:

function safeDecrypt(ciphertext, key, iv) {
  try {
    const decrypted = AES.decrypt(ciphertext, key, {
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7
    })
    
    const result = decrypted.toString(CryptoJS.enc.Utf8)
    
    // 空字符串表示解密失败
    if (!result) {
      throw new Error('解密失败:密钥错误或数据损坏')
    }
    
    return result
  } catch (e) {
    console.error('解密错误:', e)
    return null
  }
}

六、总结速记 #

AES 加密核心要点 #

类型 要点
密钥长度 16/24/32 字节(128/192/256 位)
推荐模式 CBC(通用)或 GCM(最高安全)
IV 要求 16 字节,每次加密随机生成
填充方式 Pkcs7(最通用)
密钥管理 环境变量或 PBKDF2 派生

crypto-js 快速上手 #

// 最简加密
AES.encrypt(plaintext, key).toString()
 
// 最简解密
AES.decrypt(ciphertext, key).toString(CryptoJS.enc.Utf8)
 
// CBC 模式加密
AES.encrypt(plaintext, key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})
 
// 随机密钥
CryptoJS.lib.WordArray.random(16)
 
// PBKDF2 派生
CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 10000 })

Vue 3 组合式封装 #

const { encrypt, decrypt, setKey, generateIV } = useAES()
 
setKey(import.meta.env.VITE_APP_AES_KEY)
const iv = generateIV()
const encrypted = encrypt(plaintext)
const decrypted = decrypt(encrypted)

安全最佳实践 #

实践 说明
✅ 随机 IV 每次加密生成新的 IV
✅ HMAC 签名 防止密文被篡改
✅ 时间戳 防止重放攻击
✅ PBKDF2 从密码派生强密钥
✅ HTTPS 前端加密 + HTTPS 双保险
❌ ECB 模式 永远不要用
❌ 硬编码密钥 不要写在代码里

附录 #

crypto-js API 速查 #

// AES
AES.encrypt(plaintext, key, options)  // 加密
AES.decrypt(ciphertext, key, options) // 解密
 
// 编码器
CryptoJS.enc.Utf8.parse(str)   // UTF-8 编码
CryptoJS.enc.Base64.parse(str) // Base64 编码
CryptoJS.enc.Hex.parse(str)    // Hex 编码
 
// 随机生成
CryptoJS.lib.WordArray.random(n) // n 字节随机数
 
// PBKDF2
CryptoJS.PBKDF2(password, salt, options)
 
// HMAC
CryptoJS.HmacSHA256(data, key)
 
// SHA
CryptoJS.SHA256(data)
CryptoJS.SHA512(data)
 
// MD5(不推荐用于安全)
CryptoJS.MD5(data)

推荐资源 #

资源 链接
crypto-js GitHub https://github.com/brix/crypto-js
crypto-js 文档 https://cryptojs.gitbook.io/docs/
Web Crypto API https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
AES 标准 https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

最后更新:2026-04-01