文章
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-js2.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 到服务器,服务器返回加密的 sessionKey3.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: 前端加密后,后端怎么解密? #
前后端需要约定:
- 相同的密钥(或密钥协商机制)
- 相同的模式(CBC 推荐)
- 相同的填充(Pkcs7 推荐)
- 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