Vue 3 与 Web Crypto API
概述
Web Crypto API 是一个提供强大加密功能的 Web 标准 API,允许 Web 应用程序执行各种加密操作,如生成密钥、加密解密数据、数字签名和验证等。在 Vue 3 应用中集成 Web Crypto API 可以保护敏感数据,实现安全的通信和存储,增强应用的安全性。
核心知识
1. Web Crypto API 基本概念
- 作用:提供加密、解密、数字签名、密钥生成等安全功能
- 支持的算法:
- 对称加密:AES-CBC、AES-GCM、AES-CTR 等
- 非对称加密:RSA-OAEP、ECDSA、Ed25519 等
- 哈希函数:SHA-1、SHA-256、SHA-384、SHA-512 等
- 密钥派生:PBKDF2、HKDF 等
- 随机数生成:Crypto.getRandomValues()
- 安全上下文:只能在 HTTPS 环境中使用
2. Web Crypto API 核心接口
- Crypto:提供基本的加密功能,如随机数生成
- CryptoKey:表示加密密钥
- CryptoKeyPair:表示非对称加密的密钥对(公钥和私钥)
3. Web Crypto API 工作流程
3.1 对称加密流程
- 生成对称密钥
- 准备要加密的数据
- 使用密钥加密数据
- 存储或传输加密数据
- 使用相同密钥解密数据
3.2 非对称加密流程
- 生成密钥对(公钥和私钥)
- 使用公钥加密数据
- 存储或传输加密数据
- 使用私钥解密数据
3.3 数字签名流程
- 生成密钥对
- 准备要签名的数据
- 使用私钥对数据进行签名
- 存储或传输数据和签名
- 使用公钥验证签名
4. 前端实现(Vue 3)
4.1 随机数生成
<template>
<div>
<h2>生成随机数</h2>
<button @click="generateRandomNumber">生成随机数</button>
<div v-if="randomNumber">随机数:{{ randomNumber }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const randomNumber = ref(null);
const generateRandomNumber = () => {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
randomNumber.value = array[0];
};
</script>4.2 哈希函数(SHA-256)
<template>
<div>
<h2>计算哈希值</h2>
<input v-model="inputText" placeholder="输入要哈希的文本" />
<button @click="calculateHash" :disabled="!inputText">计算 SHA-256 哈希</button>
<div v-if="hashResult">哈希值:{{ hashResult }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const inputText = ref('');
const hashResult = ref('');
const calculateHash = async () => {
try {
// 将文本转换为 ArrayBuffer
const encoder = new TextEncoder();
const data = encoder.encode(inputText.value);
// 计算哈希值
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// 将 ArrayBuffer 转换为十六进制字符串
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
hashResult.value = hashHex;
} catch (error) {
console.error('计算哈希失败:', error);
}
};
</script>4.3 对称加密(AES-GCM)
<template>
<div>
<h2>对称加密(AES-GCM)</h2>
<input v-model="plaintext" placeholder="要加密的文本" />
<button @click="encryptData" :disabled="!plaintext">加密</button>
<button @click="decryptData" :disabled="!encryptedData">解密</button>
<div v-if="encryptedData">
<h3>加密后:</h3>
<p>密文:{{ encryptedData.ciphertext }}</p>
<p>IV:{{ encryptedData.iv }}</p>
<p>Tag:{{ encryptedData.tag }}</p>
</div>
<div v-if="decryptedText">
<h3>解密后:</h3>
<p>{{ decryptedText }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const plaintext = ref('');
const encryptedData = ref(null);
const decryptedText = ref('');
let encryptionKey = null;
// 生成对称密钥
const generateKey = async () => {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true, // 是否可提取
['encrypt', 'decrypt'] // 密钥用途
);
};
// 加密数据
const encryptData = async () => {
try {
if (!encryptionKey) {
encryptionKey = await generateKey();
}
// 创建随机 IV(初始化向量)
const iv = crypto.getRandomValues(new Uint8Array(12));
// 将文本转换为 ArrayBuffer
const encoder = new TextEncoder();
const data = encoder.encode(plaintext.value);
// 加密数据
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
tagLength: 128 // 标签长度(bits)
},
encryptionKey,
data
);
// 提取认证标签(GCM 模式会生成标签)
const encryptedArray = new Uint8Array(encrypted);
const tag = encryptedArray.slice(-16); // 最后 16 字节是标签
const ciphertext = encryptedArray.slice(0, -16); // 前面的是密文
// 转换为十六进制字符串以便显示和传输
encryptedData.value = {
ciphertext: arrayBufferToHex(ciphertext),
iv: arrayBufferToHex(iv),
tag: arrayBufferToHex(tag)
};
decryptedText.value = null;
} catch (error) {
console.error('加密失败:', error);
}
};
// 解密数据
const decryptData = async () => {
try {
if (!encryptionKey || !encryptedData.value) return;
// 将十六进制字符串转换为 ArrayBuffer
const ciphertext = hexToArrayBuffer(encryptedData.value.ciphertext);
const iv = hexToArrayBuffer(encryptedData.value.iv);
const tag = hexToArrayBuffer(encryptedData.value.tag);
// 合并密文和标签
const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
combined.set(new Uint8Array(ciphertext), 0);
combined.set(new Uint8Array(tag), ciphertext.byteLength);
// 解密数据
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
tagLength: 128
},
encryptionKey,
combined
);
// 将 ArrayBuffer 转换为文本
const decoder = new TextDecoder();
decryptedText.value = decoder.decode(decrypted);
} catch (error) {
console.error('解密失败:', error);
}
};
// 辅助函数:ArrayBuffer 转十六进制字符串
const arrayBufferToHex = (buffer) => {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
};
// 辅助函数:十六进制字符串转 ArrayBuffer
const hexToArrayBuffer = (hex) => {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes.buffer;
};
</script>4.3 非对称加密(RSA-OAEP)
<template>
<div>
<h2>非对称加密(RSA-OAEP)</h2>
<input v-model="plaintext" placeholder="输入要加密的文本" />
<button @click="generateKeyPair" :disabled="keyPair">生成密钥对</button>
<button @click="encryptWithPublicKey" :disabled="!plaintext || !keyPair">使用公钥加密</button>
<button @click="decryptWithPrivateKey" :disabled="!encryptedText || !keyPair">使用私钥解密</button>
<div v-if="keyPair">
<h3>密钥对已生成</h3>
</div>
<div v-if="encryptedText">
<h3>加密后:</h3>
<p>{{ encryptedText }}</p>
</div>
<div v-if="decryptedText">
<h3>解密后:</h3>
<p>{{ decryptedText }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const plaintext = ref('');
const encryptedText = ref('');
const decryptedText = ref('');
const keyPair = ref(null);
// 生成 RSA 密钥对
const generateKeyPair = async () => {
try {
keyPair.value = await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-256'
},
true,
['encrypt', 'decrypt']
);
} catch (error) {
console.error('生成密钥对失败:', error);
}
};
// 使用公钥加密
const encryptWithPublicKey = async () => {
try {
const encoder = new TextEncoder();
const data = encoder.encode(plaintext.value);
const encrypted = await crypto.subtle.encrypt(
{
name: 'RSA-OAEP'
},
keyPair.value.publicKey,
data
);
encryptedText.value = arrayBufferToBase64(encrypted);
decryptedText.value = null;
} catch (error) {
console.error('加密失败:', error);
}
};
// 使用私钥解密
const decryptWithPrivateKey = async () => {
try {
const encrypted = base64ToArrayBuffer(encryptedText.value);
const decrypted = await crypto.subtle.decrypt(
{
name: 'RSA-OAEP'
},
keyPair.value.privateKey,
encrypted
);
const decoder = new TextDecoder();
decryptedText.value = decoder.decode(decrypted);
} catch (error) {
console.error('解密失败:', error);
}
};
// 辅助函数:ArrayBuffer 转 Base64
const arrayBufferToBase64 = (buffer) => {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
// 辅助函数:Base64 转 ArrayBuffer
const base64ToArrayBuffer = (base64) => {
const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
};
</script>5. 创建可复用的 Web Crypto Composable
// composables/useCrypto.js
import { ref } from 'vue';
export function useCrypto() {
// 辅助函数:ArrayBuffer 转十六进制字符串
const arrayBufferToHex = (buffer) => {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
};
// 辅助函数:十六进制字符串转 ArrayBuffer
const hexToArrayBuffer = (hex) => {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes.buffer;
};
// 辅助函数:ArrayBuffer 转 Base64
const arrayBufferToBase64 = (buffer) => {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
// 辅助函数:Base64 转 ArrayBuffer
const base64ToArrayBuffer = (base64) => {
const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
};
// 生成随机数
const generateRandomNumber = (bits = 32) => {
const bytes = Math.ceil(bits / 8);
const array = new Uint8Array(bytes);
crypto.getRandomValues(array);
return parseInt(arrayBufferToHex(array), 16);
};
// 生成随机字符串
const generateRandomString = (length = 16) => {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const array = new Uint32Array(length);
crypto.getRandomValues(array);
for (let i = 0; i < length; i++) {
result += charset[array[i] % charset.length];
}
return result;
};
// 计算哈希值
const computeHash = async (data, algorithm = 'SHA-256') => {
const encoder = new TextEncoder();
const buffer = typeof data === 'string' ? encoder.encode(data) : data;
const hash = await crypto.subtle.digest(algorithm, buffer);
return arrayBufferToHex(hash);
};
// 对称加密(AES-GCM)
const encryptAESGCM = async (data, key, iv = null) => {
if (!iv) {
iv = crypto.getRandomValues(new Uint8Array(12));
}
const encoder = new TextEncoder();
const buffer = typeof data === 'string' ? encoder.encode(data) : data;
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
tagLength: 128
},
key,
buffer
);
const encryptedArray = new Uint8Array(encrypted);
const tag = encryptedArray.slice(-16);
const ciphertext = encryptedArray.slice(0, -16);
return {
ciphertext: arrayBufferToBase64(ciphertext),
iv: arrayBufferToBase64(iv),
tag: arrayBufferToBase64(tag)
};
};
// 对称解密(AES-GCM)
const decryptAESGCM = async (encryptedData, key) => {
const ciphertext = base64ToArrayBuffer(encryptedData.ciphertext);
const iv = base64ToArrayBuffer(encryptedData.iv);
const tag = base64ToArrayBuffer(encryptedData.tag);
const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
combined.set(new Uint8Array(ciphertext), 0);
combined.set(new Uint8Array(tag), ciphertext.byteLength);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
tagLength: 128
},
key,
combined
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
};
// 生成 AES 密钥
const generateAESKey = async (length = 256) => {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: length
},
true,
['encrypt', 'decrypt']
);
};
// 生成 RSA 密钥对
const generateRSAKeyPair = async (modulusLength = 2048) => {
return await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: modulusLength,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-256'
},
true,
['encrypt', 'decrypt']
);
};
// 导出密钥(用于存储或传输)
const exportKey = async (key, format = 'jwk') => {
return await crypto.subtle.exportKey(format, key);
};
// 导入密钥
const importKey = async (keyData, algorithm, usages, format = 'jwk') => {
return await crypto.subtle.importKey(
format,
keyData,
algorithm,
true,
usages
);
};
return {
generateRandomNumber,
generateRandomString,
computeHash,
encryptAESGCM,
decryptAESGCM,
generateAESKey,
generateRSAKeyPair,
exportKey,
importKey,
arrayBufferToHex,
hexToArrayBuffer,
arrayBufferToBase64,
base64ToArrayBuffer
};
}最佳实践
1. 安全考虑
- 始终使用 HTTPS:Web Crypto API 只能在安全上下文(HTTPS)中使用
- 保护密钥:不要在客户端代码中硬编码密钥
- 使用强算法:优先使用 AES-GCM、RSA-OAEP 等现代算法,避免使用过时的 DES、RC4 等
- 安全存储密钥:考虑使用 Web Crypto API 的密钥存储功能,或加密后存储在 IndexedDB 中
- 使用适当的密钥长度:AES 至少使用 128 位,RSA 至少使用 2048 位
2. 性能优化
- 重用密钥:避免频繁生成新密钥,特别是非对称密钥
- 合理使用加密:只加密敏感数据,不要加密所有数据
- 考虑异步操作:Web Crypto API 的大部分方法都是异步的,使用 async/await 处理
- 优化数据转换:减少 ArrayBuffer、Base64、十六进制之间的转换次数
3. 错误处理
- 捕获和处理所有错误:加密操作可能会抛出各种错误,如不支持的算法、无效的密钥等
- 提供友好的错误信息:向用户显示清晰的错误信息,而不是原始错误
- 记录错误:在开发环境中记录详细的错误信息,便于调试
4. 跨浏览器兼容性
- 检查浏览器支持:在使用前检查 Web Crypto API 是否可用
- 使用标准算法:优先使用广泛支持的算法
- 测试多种浏览器:在不同浏览器上测试加密功能
常见问题与解决方案
1. 加密数据过大导致失败
- 原因:RSA 加密有最大数据大小限制(取决于密钥长度)
- 解决方案:
- 对于大文件,使用对称加密(AES)
- 或使用混合加密:生成随机对称密钥,用对称密钥加密数据,用 RSA 加密对称密钥
2. 密钥无法导出
- 原因:生成密钥时
extractable参数设置为false - 解决方案:生成密钥时将
extractable参数设置为true
3. 解密失败,报错 "OperationError"
- 原因:
- 密钥不匹配
- IV(初始化向量)不正确
- 认证标签(GCM 模式)不匹配
- 密文被篡改
- 解决方案:
- 检查密钥是否正确
- 确保 IV 和认证标签与加密时使用的相同
- 验证密文完整性
4. 浏览器不支持某些算法
- 原因:不同浏览器对 Web Crypto API 算法的支持程度不同
- 解决方案:
- 检查浏览器支持的算法
- 提供备选算法
- 使用特性检测
5. 密钥存储安全问题
- 原因:客户端存储密钥存在安全风险
- 解决方案:
- 考虑使用 Web Crypto API 的密钥存储功能
- 加密后存储在 IndexedDB 中
- 对于敏感数据,考虑在服务器端加密
高级学习资源
1. 官方文档
2. 深度教程
3. 相关库和工具
4. 视频教程
实践练习
1. 基础练习:实现简单的加密功能
- 创建 Vue 3 应用,实现随机数生成
- 实现哈希函数计算
- 测试不同的哈希算法(SHA-1、SHA-256、SHA-512)
2. 进阶练习:对称加密应用
- 实现 AES-GCM 对称加密和解密
- 创建密钥生成和导出功能
- 实现加密数据的存储和读取
3. 高级练习:非对称加密应用
- 实现 RSA-OAEP 非对称加密和解密
- 实现数字签名和验证
- 测试密钥对生成和管理
4. 综合练习:密码管理器
- 创建简单的密码管理器应用
- 使用 Web Crypto API 加密存储密码
- 实现密码生成、加密、解密功能
- 添加用户认证
5. 挑战练习:安全聊天应用
- 创建基于 WebSocket 的聊天应用
- 使用 Web Crypto API 加密消息
- 实现端到端加密
- 支持密钥协商和管理
总结
Web Crypto API 为 Vue 3 应用提供了强大的加密功能,可以显著增强应用的安全性。通过合理使用 Web Crypto API 的各种功能,如对称加密、非对称加密、数字签名等,可以保护用户的敏感数据,实现安全的通信和存储。掌握 Web Crypto API 的实现原理和最佳实践,对于构建现代化、安全的 Vue 3 应用至关重要。