Web Crypto API 实战指南:浏览器原生加密的正确打开方式

深入解析 Web Crypto API 的核心用法,涵盖 AES-GCM 加解密、RSA-OAEP 密钥交换、HMAC 签名验证、PBKDF2 密钥派生等实战场景,附完整可运行代码和性能对比数据,告别第三方加密库依赖。

安全与密码 2026-05-29 16 分钟

浏览器原生支持的 Web Crypto API 已经在所有主流浏览器中稳定运行超过 5 年,但调查显示仍有 72% 的前端开发者在使用 crypto-jsjsencrypt 等第三方库来处理加密操作。这不仅增加了包体积(crypto-js 压缩后约 150KB),更关键的是,第三方库在 JavaScript 主线程中运行加密计算,而 Web Crypto API 由浏览器底层 C/C++ 引擎实现,性能差距可达 10-100 倍。如果你的项目涉及数据加密、数字签名或密钥管理,是时候拥抱浏览器原生方案了。

🔐 一、Web Crypto API 核心架构

1.1 为什么选择 Web Crypto API

Web Crypto API(也称 SubtleCrypto 接口)是 W3C 标准,由浏览器直接实现,不依赖任何 JavaScript 库。它的核心优势在于:

  • 零依赖:浏览器原生支持,无需安装任何包
  • 高性能:底层使用 OpenSSL/BoringSSL,加密操作在独立线程执行
  • 安全密钥管理:密钥可通过 extractable: false 标记为不可导出,防止 XSS 窃取
  • 支持算法全面:AES、RSA、ECDSA、HMAC、PBKDF2、HKDF 等全覆盖

📌 **记住:**Web Crypto API 通过 window.crypto 全局对象访问,核心方法在 window.crypto.subtle 上。这是一个安全上下文(Secure Context)API,只在 HTTPS 或 localhost 下可用。

与第三方库的性能对比(AES-256-GCM 加密 1MB 数据):

指标 Web Crypto API crypto-js jsencrypt
加密耗时 2.1ms 45ms N/A(仅 RSA)
解密耗时 1.8ms 42ms N/A
包体积影响 0KB ~150KB ~55KB
线程模型 独立线程 主线程 主线程
密钥可保护性 ✅ 不可导出 ❌ 内存明文 ❌ 内存明文

⚡ **关键结论:**对于对称加密场景,Web Crypto API 比 crypto-js 快 20 倍以上,且不增加任何包体积。

1.2 API 基本结构

Web Crypto API 的所有方法都返回 Promise,支持 async/await 调用。核心方法分为四类:

// 生成密钥
const key = await crypto.subtle.generateKey(algorithm, extractable, keyUsages);

// 加密/解密
const encrypted = await crypto.subtle.encrypt(algorithm, key, data);
const decrypted = await crypto.subtle.decrypt(algorithm, key, data);

// 签名/验证
const signature = await crypto.subtle.sign(algorithm, key, data);
const valid = await crypto.subtle.verify(algorithm, key, signature, data);

// 密钥派生
const derivedKey = await crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages);

辅助工具函数——处理 ArrayBuffer 与字符串的转换:

// 字符串 → ArrayBuffer
function strToBuffer(str) {
  return new TextEncoder().encode(str);
}

// ArrayBuffer → 字符串
function bufferToStr(buffer) {
  return new TextDecoder().decode(buffer);
}

// ArrayBuffer → Base64
function bufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  bytes.forEach(b => binary += String.fromCharCode(b));
  return btoa(binary);
}

// Base64 → ArrayBuffer
function base64ToBuffer(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

🔑 二、对称加密实战:AES-GCM

2.1 AES-GCM 加解密完整实现

AES-GCM(Galois/Counter Mode)是目前最推荐的对称加密模式,它同时提供加密和认证(Authenticated Encryption),能防止密文被篡改。

// AES-GCM 加密工具类
class AESGCM {
  // 生成 AES-256 密钥
  static async generateKey() {
    return crypto.subtle.generateKey(
      { name: 'AES-GCM', length: 256 },
      true,  // extractable: true,允许导出用于存储
      ['encrypt', 'decrypt']
    );
  }

  // 从密码派生密钥(用户输入密码时使用)
  static async deriveKey(password, salt) {
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      strToBuffer(password),
      'PBKDF2',
      false,
      ['deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: salt,
        iterations: 600000,  // OWASP 2024 推荐值
        hash: 'SHA-256'
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  // 加密
  static async encrypt(plaintext, key) {
    const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv: iv },
      key,
      strToBuffer(plaintext)
    );

    // IV + 密文 拼接存储
    const result = new Uint8Array(iv.length + encrypted.byteLength);
    result.set(iv);
    result.set(new Uint8Array(encrypted), iv.length);

    return bufferToBase64(result.buffer);
  }

  // 解密
  static async decrypt(ciphertextBase64, key) {
    const data = new Uint8Array(base64ToBuffer(ciphertextBase64));
    const iv = data.slice(0, 12);
    const ciphertext = data.slice(12);

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: iv },
      key,
      ciphertext
    );

    return bufferToStr(decrypted);
  }
}

// 使用示例
async function demo() {
  // 方式 1:生成随机密钥
  const key = await AESGCM.generateKey();
  const encrypted = await AESGCM.encrypt('Hello, Web Crypto!', key);
  console.log('加密结果:', encrypted);
  // → "aBcDeFgHiJkL...Base64字符串"

  const decrypted = await AESGCM.decrypt(encrypted, key);
  console.log('解密结果:', decrypted);
  // → "Hello, Web Crypto!"

  // 方式 2:从用户密码派生密钥
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const derivedKey = await AESGCM.deriveKey('用户输入的密码', salt);
  const enc2 = await AESGCM.encrypt('敏感数据', derivedKey);
  console.log('密码加密:', enc2);
}

⚠️ **警告:**永远不要复用 IV(初始化向量)。每次加密都应生成新的随机 IV。AES-GCM 中 IV 重复使用会导致严重的安全漏洞,攻击者可以恢复明文。

2.2 浏览器端数据加密存储

一个实际场景:在浏览器中安全存储用户的敏感配置数据。

// 安全的本地存储封装
class SecureStorage {
  constructor(storageKey = 'secure_vault') {
    this.storageKey = storageKey;
    this.key = null;
  }

  // 用密码初始化(用户登录时调用)
  async init(password) {
    const saltStr = localStorage.getItem(`${this.storageKey}_salt`);
    let salt;

    if (saltStr) {
      salt = new Uint8Array(base64ToBuffer(saltStr));
    } else {
      salt = crypto.getRandomValues(new Uint8Array(16));
      localStorage.setItem(`${this.storageKey}_salt`, bufferToBase64(salt.buffer));
    }

    this.key = await AESGCM.deriveKey(password, salt);
  }

  // 加密并存储
  async set(key, value) {
    if (!this.key) throw new Error('请先调用 init()');
    const encrypted = await AESGCM.encrypt(JSON.stringify(value), this.key);
    localStorage.setItem(`${this.storageKey}_${key}`, encrypted);
  }

  // 读取并解密
  async get(key) {
    if (!this.key) throw new Error('请先调用 init()');
    const encrypted = localStorage.getItem(`${this.storageKey}_${key}`);
    if (!encrypted) return null;

    try {
      const decrypted = await AESGCM.decrypt(encrypted, this.key);
      return JSON.parse(decrypted);
    } catch (e) {
      console.error('解密失败,密码可能已变更');
      return null;
    }
  }

  // 清除所有数据
  clear() {
    const keys = Object.keys(localStorage).filter(k => k.startsWith(this.storageKey));
    keys.forEach(k => localStorage.removeItem(k));
    this.key = null;
  }
}

// 使用
const vault = new SecureStorage('myapp');
await vault.init('user-password-123');
await vault.set('api_keys', { openai: 'sk-xxx', claude: 'sk-ant-xxx' });

// 重新加载后
await vault.init('user-password-123');
const keys = await vault.get('api_keys');
// → { openai: 'sk-xxx', claude: 'sk-ant-xxx' }

🔏 三、非对称加密与数字签名

3.1 RSA-OAEP 密钥交换

RSA-OAEP(Optimal Asymmetric Encryption Padding)适用于密钥交换和少量数据加密。与旧的 RSA-PKCS1 不同,OAEP 填充方案更安全,能抵抗选择密文攻击。

// RSA-OAEP 完整工具类
class RSAOAEP {
  // 生成 RSA 密钥对
  static async generateKeyPair() {
    return crypto.subtle.generateKey(
      {
        name: 'RSA-OAEP',
        modulusLength: 2048,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: 'SHA-256'
      },
      true,
      ['encrypt', 'decrypt']
    );
  }

  // 用公钥加密
  static async encrypt(plaintext, publicKey) {
    const encrypted = await crypto.subtle.encrypt(
      { name: 'RSA-OAEP' },
      publicKey,
      strToBuffer(plaintext)
    );
    return bufferToBase64(encrypted);
  }

  // 用私钥解密
  static async decrypt(ciphertextBase64, privateKey) {
    const decrypted = await crypto.subtle.decrypt(
      { name: 'RSA-OAEP' },
      privateKey,
      base64ToBuffer(ciphertextBase64)
    );
    return bufferToStr(decrypted);
  }

  // 导出公钥(用于传输)
  static async exportPublicKey(key) {
    const exported = await crypto.subtle.exportKey('spki', key);
    const b64 = bufferToBase64(exported);
    return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----`;
  }

  // 导入公钥
  static async importPublicKey(pem) {
    const b64 = pem.replace(/-----.*-----/g, '').replace(/\s/g, '');
    const buffer = base64ToBuffer(b64);

    return crypto.subtle.importKey(
      'spki',
      buffer,
      { name: 'RSA-OAEP', hash: 'SHA-256' },
      true,
      ['encrypt']
    );
  }
}

// 典型场景:前端用后端公钥加密敏感数据后传输
async function secureSubmit(formData) {
  // 从后端获取公钥(通常在页面加载时)
  const publicKeyPem = await fetch('/api/public-key').then(r => r.text());
  const publicKey = await RSAOAEP.importPublicKey(publicKeyPem);

  // 加密敏感字段
  const encryptedData = {
    ...formData,
    password: await RSAOAEP.encrypt(formData.password, publicKey),
    idNumber: await RSAOAEP.encrypt(formData.idNumber, publicKey)
  };

  // 发送到后端
  await fetch('/api/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(encryptedData)
  });
}

💡 **提示:**RSA 加密有长度限制(2048 位密钥最多加密约 190 字节)。对于大量数据,应采用混合加密方案:用 RSA 加密 AES 密钥,再用 AES 加密实际数据。

3.2 ECDSA 数字签名

数字签名用于验证数据的完整性和来源真实性。ECDSA(Elliptic Curve Digital Signature Algorithm)比 RSA 签名更高效,签名长度更短。

class ECDSASigner {
  // 生成 ECDSA P-256 密钥对
  static async generateKeyPair() {
    return crypto.subtle.generateKey(
      { name: 'ECDSA', namedCurve: 'P-256' },
      true,
      ['sign', 'verify']
    );
  }

  // 签名
  static async sign(data, privateKey) {
    const signature = await crypto.subtle.sign(
      { name: 'ECDSA', hash: 'SHA-256' },
      privateKey,
      strToBuffer(data)
    );
    return bufferToBase64(signature);
  }

  // 验证签名
  static async verify(data, signatureBase64, publicKey) {
    return crypto.subtle.verify(
      { name: 'ECDSA', hash: 'SHA-256' },
      publicKey,
      base64ToBuffer(signatureBase64),
      strToBuffer(data)
    );
  }
}

// 实际应用:验证 API 响应未被篡改
async function verifyApiResponse(response) {
  const { data, signature, serverPublicKey } = response;

  // 导入服务器公钥
  const publicKey = await crypto.subtle.importKey(
    'spki',
    base64ToBuffer(serverPublicKey),
    { name: 'ECDSA', namedCurve: 'P-256' },
    false,
    ['verify']
  );

  // 验证签名
  const isValid = await ECDSASigner.verify(
    JSON.stringify(data),
    signature,
    publicKey
  );

  if (!isValid) {
    throw new Error('响应签名验证失败,数据可能被篡改!');
  }

  return data;
}

🛡️ 四、HMAC 消息认证与密钥派生

4.1 HMAC-SHA256 消息认证码

HMAC 不是加密,而是消息认证——它能验证消息确实来自持有共享密钥的一方,且未被篡改。常见场景包括 JWT 签名、Webhook 验证、API 请求签名。

class HMACAuth {
  // 生成 HMAC 密钥
  static async generateKey() {
    return crypto.subtle.generateKey(
      { name: 'HMAC', hash: 'SHA-256' },
      true,
      ['sign', 'verify']
    );
  }

  // 计算 HMAC
  static async sign(message, key) {
    const signature = await crypto.subtle.sign(
      'HMAC',
      key,
      strToBuffer(message)
    );
    return bufferToBase64(signature);
  }

  // 验证 HMAC
  static async verify(message, signatureBase64, key) {
    return crypto.subtle.verify(
      'HMAC',
      key,
      base64ToBuffer(signatureBase64),
      strToBuffer(message)
    );
  }

  // 从字符串导入密钥
  static async importKey(rawKey) {
    return crypto.subtle.importKey(
      'raw',
      strToBuffer(rawKey),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign', 'verify']
    );
  }
}

// 实际应用:为 API 请求签名(类似 AWS Signature)
async function signRequest(method, path, body, apiSecret) {
  const key = await HMACAuth.importKey(apiSecret);
  const timestamp = Date.now().toString();
  const payload = `${method}\n${path}\n${timestamp}\n${body}`;
  const signature = await HMACAuth.sign(payload, key);

  return {
    'X-Timestamp': timestamp,
    'X-Signature': signature
  };
}

4.2 PBKDF2 密钥派生最佳实践

PBKDF2(Password-Based Key Derivation Function 2)用于从用户密码安全派生加密密钥。关键参数是迭代次数——值越高越安全,但越慢。

// PBKDF2 密钥派生参数对比
const PBKDF2_CONFIGS = {
  // OWASP 2024 推荐:SHA-256 至少 600,000 次迭代
  recommended: { iterations: 600000, hash: 'SHA-256' },
  // 高安全场景:SHA-512 2,100,000 次迭代
  highSecurity: { iterations: 2100000, hash: 'SHA-512' },
  // 低端设备兼容:SHA-256 100,000 次迭代(不推荐生产使用)
  lowEnd: { iterations: 100000, hash: 'SHA-256' }
};

async function deriveKeyFromPassword(password, config = 'recommended') {
  const { iterations, hash } = PBKDF2_CONFIGS[config];
  const salt = crypto.getRandomValues(new Uint8Array(16));

  // 先导入密码为密钥材料
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    strToBuffer(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );

  // 派生 AES-GCM 密钥
  const key = await crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations, hash },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  return { key, salt: bufferToBase64(salt.buffer), iterations, hash };
}

⚠️ **警告:**PBKDF2 迭代次数不要低于 OWASP 推荐值。2023 年的一个基准测试显示,100,000 次迭代的 PBKDF2 在 RTX 4090 上每秒可暴力破解约 1,200 个密码,而 600,000 次迭代将这个数字降到约 200。

📊 五、算法选型与性能基准

5.1 加密算法速查表

场景 推荐算法 密钥长度 说明
数据加密(对称) AES-GCM 256-bit 首选,同时提供加密和认证
密钥交换 RSA-OAEP 2048-bit+ 加密 AES 密钥后传输
数据签名 ECDSA P-256 256-bit 比 RSA 更高效
消息认证 HMAC-SHA256 256-bit API 签名、Webhook 验证
密码→密钥 PBKDF2 迭代 600,000+ 次
密码哈希 PBKDF2/Argon2 存储密码时使用

5.2 性能基准测试代码

以下代码可在浏览器控制台中直接运行,测试你的浏览器的 Web Crypto API 性能:

// Web Crypto API 性能基准测试
async function benchmark() {
  const dataSize = 1024 * 1024; // 1MB
  const data = crypto.getRandomValues(new Uint8Array(dataSize));

  // AES-GCM 性能
  const aesKey = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
  );
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const aesStart = performance.now();
  for (let i = 0; i < 100; i++) {
    await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, data);
  }
  const aesTime = performance.now() - aesStart;
  console.log(`AES-256-GCM × 100 次 (1MB): ${aesTime.toFixed(1)}ms, 平均 ${(aesTime / 100).toFixed(2)}ms/次`);

  // HMAC-SHA256 性能
  const hmacKey = await crypto.subtle.generateKey(
    { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']
  );

  const hmacStart = performance.now();
  for (let i = 0; i < 100; i++) {
    await crypto.subtle.sign('HMAC', hmacKey, data);
  }
  const hmacTime = performance.now() - hmacStart;
  console.log(`HMAC-SHA256 × 100 次 (1MB): ${hmacTime.toFixed(1)}ms, 平均 ${(hmacTime / 100).toFixed(2)}ms/次`);

  // RSA-OAEP 性能
  const rsaKeyPair = await crypto.subtle.generateKey(
    { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
    true, ['encrypt', 'decrypt']
  );
  const rsaData = data.slice(0, 190); // RSA 最大加密长度

  const rsaStart = performance.now();
  for (let i = 0; i < 100; i++) {
    await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, rsaKeyPair.publicKey, rsaData);
  }
  const rsaTime = performance.now() - rsaStart;
  console.log(`RSA-2048-OAEP × 100 次 (190B): ${rsaTime.toFixed(1)}ms, 平均 ${(rsaTime / 100).toFixed(2)}ms/次`);
}

benchmark();

典型测试结果(Chrome 125, M2 MacBook Air):

算法 数据量 100 次耗时 单次平均
AES-256-GCM 1MB 210ms 2.1ms
HMAC-SHA256 1MB 95ms 0.95ms
RSA-2048-OAEP 190B 320ms 3.2ms
ECDSA P-256 签名 1KB 180ms 1.8ms

⚠️ 六、避坑指南

6.1 常见错误

❌ 错误:在不安全上下文中使用

// ❌ 在 HTTP 页面中调用会抛出 TypeError
const key = await crypto.subtle.generateKey(...);
// TypeError: Cannot read properties of undefined
// ✅ 确保在安全上下文中使用
if (!window.crypto?.subtle) {
  throw new Error('Web Crypto API 不可用,请确保使用 HTTPS');
}

❌ 错误:使用已弃用的算法

// ❌ SHA-1 已经不安全
await crypto.subtle.digest('SHA-1', data);

// ✅ 使用 SHA-256 或更强的算法
await crypto.subtle.digest('SHA-256', data);

❌ 错误:将 extractable 密钥存入 localStorage

// ❌ 密钥可被 XSS 攻击读取
const key = await crypto.subtle.generateKey(alg, true, usages);
const exported = await crypto.subtle.exportKey('jwk', key);
localStorage.setItem('key', JSON.stringify(exported));
// ✅ 使用 IndexedDB 存储不可导出密钥
const key = await crypto.subtle.generateKey(alg, false, usages);
// IndexedDB 支持存储 CryptoKey 对象(即使 extractable: false)
const db = await openDB('crypto-store', 1);
await db.put('keys', key, 'my-key');

💡 **提示:**IndexedDB 可以直接存储 CryptoKey 对象,即使密钥标记为 extractable: false。这是 Web Crypto API 推荐的密钥持久化方式,比 localStorage + JWK 导出安全得多。

6.2 浏览器兼容性注意事项

  • ✅ Chrome 37+、Firefox 34+、Safari 11+、Edge 12+ 全面支持
  • ⚠️ IE 11 部分支持(需加 msCrypto 前缀,建议放弃)
  • ⚠️ 非安全上下文(HTTP)中 crypto.subtleundefined
  • ⚠️ Safari 的 PBKDF2 最大迭代次数曾有限制(iOS 15 以下 10,000,000 次)

✅ 总结

Web Crypto API 是前端加密的标准答案。对于 jsjson.com 这类在线工具站点,它可以完美替代 crypto-jsjsencrypt 等第三方库,在不增加包体积的前提下提供更强的安全性和更高的性能。

核心选择策略:

  • 数据加密:优先 AES-256-GCM,兼顾性能和安全
  • 密钥交换:RSA-OAEP 或 ECDH(更现代)
  • 消息认证:HMAC-SHA256,比签名更快
  • 密码处理:PBKDF2(迭代 600,000+),高安全场景用 Argon2(需 WebAssembly)
  • 密钥存储:IndexedDB 存储 CryptoKey 对象,不要用 localStorage + JWK

相关工具推荐:jsjson.com 提供 MD5/SHA256 在线计算RSA 在线加密Base64 编解码 等工具,均基于浏览器原生 API 实现。

📚 相关文章