Web Crypto API 浏览器原生加密的终极指南

2026-08-25 15:59:25
6031字
33.5分钟

在过去很长一段时间里,只要一提到在前端进行加密、解密、生成哈希(MD5/SHA)或生成随机数,绝大多数开发者的第一反应就是 npm install crypto-js。作为曾经的功勋库,crypto-js 陪伴我们度过了无数个项目。

然而,随着 Web 标准的演进,现代浏览器早已内置了一个更强大、更安全、性能也更恐怖的官方武器——Web Crypto API。时至今日,如果你还在新项目里打包第三方的加密库,是时候了解一下为什么要换成原生 API 了。

1. 什么是 Web Crypto API?

Web Crypto API 是浏览器原生提供的一套加密接口,允许 JavaScript 脚本使用密码学原语来构建基于加密的系统。它自 2015 年 7 月起已得到各大浏览器的广泛支持,是一个相当成熟且稳定的 Web 标准。

在 Web Crypto 规范诞生前,部分浏览器为了满足加密需求,在全局实现了一个名为 Crypto 的接口(即 window.crypto)。但正由于这样,当时各大浏览器的实现缺乏统一的 W3C 标准,定义模糊,且在密码学意义上是不够严谨和安全的(not cryptographically sound)。

当正式的 Web Crypto API 标准(支持 encrypt、decrypt、sign、verify 等现代异步加密操作)推出时,为了彻底避免和老旧、不安全的旧接口方法发生命名混淆与冲突,W3C 规范决定将这些低底层的密码学核心方法群全部打包进一个全新的独立接口中,这就是 SubtleCrypto。你可以通过全局对象的 crypto.subtle 属性来访问它。

由于旧的全局 crypto 对象并没有被废除(它依然保留了诸如密码学安全的伪随机数生成器 crypto.getRandomValues() 和 crypto.randomUUID() 等简单的同步方法),因此所有新版的 SubtleCrypto 复杂加密算法全部被挂载到了它的下级属性中。你必须通过 crypto.subtle 属性才能访问到这一整套现代加解密方法。

Node.js 最早在 v15.0.0 引入了 Web Crypto API 接口。直到 Node.js 19.0.0,官方正式去掉了实验性警告,将其标记为 稳定版本(Stable),并且默认在全局作用域挂载了 globalThis.crypto。

重要提示:在现代浏览器中,为了保障密钥安全,crypto.subtle 属性被限制为只能在安全上下文(HTTPS 协议或 localhost)下才能被成功访问。如果你在普通的非加密 HTTP 网页里调用 crypto.subtle,它会直接返回 undefined。

2. 为什么选择 Web Crypto API?

在决定是否在 Web 项目中使用 Web Crypto API 时,通常是因为它相比传统的第三方加密库(如 CryptoJS, jsencrypt, forge 等)在安全性、性能和原生支持度上带来了降维打击的优势。

  1. 防 XSS 攻击的“密钥黑盒”机制(核心安全优势) : 这是选择Web Crypto API最关键的安全理由。
  • 传统加密库的痛点: 如果你用 CryptoJS,密钥必须以明文字符串或 Uint8Array 的形式存在于 JavaScript 变量、localStorage 或 sessionStorage 中。一旦网站遭受 XSS(跨站脚本)攻击,黑客可以直接读取并盗走你的密钥。
  • Web Crypto 的解决办法: 它引入了 CryptoKey 对象。在生成或导入密钥时,如果设置 extractable: false,这个密钥在创建后将永远无法通过 JavaScript 读取、打印或导出。它被死死封锁在浏览器的底层安全内存中。黑客即使通过 XSS 控制了你的页面,也拿不到密钥本身,只能在本地调用它,大大降低了密钥泄露的风险。
  1. 硬件级的高性能与非阻塞(异步原生)
  • 原生 C++ / 汇编实现: Web Crypto API 是由浏览器底层(基于 OpenSSL 或操作系统自带的密码学库)用原生代码实现的。相比纯 JavaScript 编写的加密库,它的运行速度快了数十倍到上百倍。
  • 异步非阻塞: 传统的加密库(如 CryptoJS)大都是同步执行的。当计算大文件哈希,或者进行复杂的 RSA 大素数生成时,会直接卡死浏览器的 UI 主线程,导致页面瞬间卡顿或未响应。而 Web Crypto 的所有核心方法(如 encrypt、digest)全部是异步 Promise 机制,浏览器会将其交给底层的系统线程去算,完全不影响前端界面的流畅度。
  1. 密码学安全的伪随机数生成器 (CSPRNG): 在密码学中,随机数的质量决定了加密的生死。
  • 传统的隐患: JavaScript 原生的 Math.random() 是非密码学安全的,它的随机轨迹可以被算法预测,绝不能用于生成加密密钥、IV(初始化向量)或盐值。
  • Web Crypto 的保障: 提供了 crypto.getRandomValues(),它直接调用操作系统底层的硬件熵源(如系统的 /dev/urandom 或 CAPI),生成密码学安全的高强度真随机数,保障加密系统的根基不可被破解。
  1. 零依赖,极致减小打包体积 (Zero-Bundle)
  • 传统加密库的痛点: 引入 CryptoJS、JSEncrypt 或 Forge 会让前端的 Vendor 打包体积(Bundle Size)平白无故增加几十 KB 甚至几百 KB,影响首屏加载速度。
  • Web Crypto 的优势: 它是现代浏览器的标准内置 API,不需要 npm install 任何第三方包。直接开箱即用,让前端工程更加轻量、纯净。
  1. 行业标准规范,跨端兼容性完美
  • 大厂和 W3C 背书: 它遵循严格的 W3C 规范,由各大浏览器厂商共同维护和不断优化。
  • 环境通用性: 目前 Web Crypto API 的覆盖率已接近 100%。不仅在 Chrome、Safari、Edge、Firefox 浏览器中完美支持,在现代的 Node.js (v15+)、Deno、Bun 以及 Cloudflare Workers 等服务端/边缘计算环境中也实现了完全一致的 API。这使得你写的一套加解密代码,可以无缝在前端和服务端之间复用。

3. 什么时候不应该选择它?

使用 Web Crypto API 有唯一的限制条件:

  • 非安全上下文限制: 它强制要求 HTTPS 环境(本地开发支持 localhost 或 127.0.0.1)。如果你的网站部署在纯 HTTP 生产环境下,crypto.subtle 会直接返回 undefined 无法工作。
  • 需要高层封装: 它提供的是非常底层的“密码学原语”(Raw Primitives),不提供像“一键生成 JWT”或者“一键做密码信封”这样的高级功能,开发者需要自己处理字节对齐、IV 传递等微秒的细节。

4. Web Crypto API核心接口

Web Crypto API(通常称为 crypto.subtle)的核心接口主要由以下 5 个标准接口(Interfaces) 和 1 个全局入口 组成。它们构成了浏览器原生密码学生态的基石。

4.1 全局入口对象Crypto

虽然这不是单独的加解密接口,但它是所有 Web 加密功能的总入口,挂载在全局 window.crypto(或 globalThis.crypto)上。Crypto提供基础的加密功能,主要包括:

  • 核心作用: 提供基础的同步安全原语。
  • 常用方法:
    • crypto.getRandomValues(): 生成密码学安全的伪随机数(常用于生成 IV 或盐)。
    • crypto.randomUUID(): 快速生成一个符合 RFC 4122 标准的随机 UUID 字符串。
    • crypto.subtle: 指向核心异步加密接口 SubtleCrypto 的指针。

4.2 核心操作员SubtleCrypto

这是整个 Web Crypto API 中最重要、最核心的接口。所有的实际异步加解密、签名、密钥派生等操作都在这里执行。SubtleCrypto 提供的主要方法包括:

  • 核心作用: 提供一系列底层、底层的异步密码学原语方法(全部返回 Promise)。
  • 核心方法(按功能分类):
    • 加密与解密: encrypt() / decrypt()
    • 签名与验证: sign() / verify()
    • 哈希计算: digest()
    • 密钥生成: generateKey()
    • 密钥导入与导出: importKey() / exportKey()
    • 密钥包装与解包: wrapKey() / unwrapKey()
    • 密钥派生: deriveKey() / deriveBits()

4.3 密钥实体CryptoKey

该接口代表一个在浏览器中生成的、受保护的密钥对象。为了安全,浏览器的 SubtleCrypto 方法不直接操作纯文本的密钥字符串(如 "my-secret-key"),而是操作 CryptoKey 实例。

  • 核心作用: 作为参数传递给 SubtleCrypto 的各项方法。
  • 核心属性(只读):
    • type: 密钥类型("secret" 对称加密、"private" 私钥、"public" 公钥)。
    • extractable: 布尔值。如果为 false,该密钥在创建后绝对无法从内存中被导出或用 JS 打印出来,防止 XSS 攻击窃取密钥。
    • algorithm: 绑定该密钥的算法对象(例如绑定的 AES-GCM 或 RSA-PSS 等参数)。
    • usages: 该密钥被允许的用途数组(例如 'encrypt', 'decrypt')。

4.4 非对称密钥对CryptoKeyPair

当使用 RSA、ECDSA 等非对称加密算法时,crypto.subtle.generateKey() 返回的不是单个密钥,而是一个包含公钥和私钥的 CryptoKeyPair 对象。

  • 核心作用: 容器对象,封装非对称密钥对。
  • 核心属性:
    • publicKey: 一个 CryptoKey 实例,代表公钥(用于加密或验证签名)。
    • privateKey: 一个 CryptoKey 实例,代表私钥(用于解密或生成签名)。

4.5 算法配置对象AlgorithmIdentifier

虽然它在技术上是一个规范定义的字典对象(Dictionary),而不是传统的类或接口,但它是每个 API 都必传的灵魂参数。

  • 核心作用: 用来指定具体的密码学算法名称以及其特有的初始化附加参数(如 IV、盐值、计数器等)。
  • 常见表现形式:
    • 哈希配置: { name: "SHA-256" }
    • AES-GCM 配置: { name: "AES-GCM", iv: Uint8Array, tagLength: 128 }
    • PBKDF2(密码派生)配置: { name: "PBKDF2", salt: Uint8Array, iterations: 100000, hash: "SHA-256" }

4.6 核心接口对比速查表

接口名称角色定位非安全上下文(HTTP)使用规则典型返回值 / 用途
Crypto全局大管家是(部分方法受限)提供crypto.subtle入口、用于生成UUID
SubtleCrypto核心工具库否(必须在HTTPS/localhost环境下使用)返回Promise<ArrayBuffer>,用于生成密文、哈希等加密核心操作
CryptoKey密钥黑盒作为安全载体,直接传递给加解密函数使用
CryptoKeyPair密钥对容器封装返回包含publicKey和privateKey的密钥对对象

5. Web Crypto API支持的主流算法

  1. 对称加密(Symmetric)表
算法标识符支持的操作推荐搭配与技术参数典型应用场景
AES-GCM(强烈推荐)encrypt、decrypt、generateKey、importKey、exportKey、wrapKey、unwrapKey密钥支持128/192/256位,IV推荐96位(12字节)传输数据加解密、本地缓存加密,自带防篡改认证标签
AES-CBCencrypt、decrypt、generateKey、importKey、exportKey、wrapKey、unwrapKey密钥支持128/192/256位,IV固定128位(16字节)兼容老旧遗留系统的对称加解密业务
AES-CTRencrypt、decrypt、generateKey、importKey、exportKey、wrapKey、unwrapKey计数器模式,支持流式处理高频非对齐的流式数据区块加解密
  • AES-GCM 是当前Web场景下的首选方案,硬件加速支持度高,同时自带消息认证能力,无需额外实现哈希校验逻辑,安全性和开发效率都更优。
  • 不同算法的IV长度要求严格,比如AES-CBC必须使用固定16字节的IV,否则会直接触发解密失败。
  • 三款算法都完整覆盖了密钥生成、导入导出、包裹/解包裹密钥的全流程操作,完全适配Web Crypto的全场景开发需求。
  1. 非对称加密算法(Asymmetric)表
算法标识符支持的操作推荐搭配与技术参数典型应用场景
RSA-OAEP(强烈推荐)encrypt, decrypt, generateKey, importKey, exportKey, wrapKey, unwrapKey模数:2048, 4096 位哈希:推荐 SHA-256混合加密体系中用于加密“对称密钥”,或加密超短敏感字段

RSA-OAEP是Web场景下非对称加密的首选方案,它通过最优非对称加密填充机制,解决了传统“教科书RSA”的密文可延展性安全缺陷,在随机预言模型下可以抵御适应性选择密文攻击。由于非对称加密的特性,它不适合直接加密大体积数据,行业通用的最佳实践就是用它加密AES等对称密钥,再用对称算法加密实际业务数据,兼顾安全性和性能效率。

  1. 数字签名算法(Signature)表
算法名称支持的操作推荐搭配与技术参数典型应用场景
ECDSA(强烈推荐)sign、verify、generateKey、importKey、exportKey椭圆曲线:P-256、P-384、P-521现代高并发身份验签、JWT签名、区块链节点验签
RSASSA-PKCS1-v1_5sign、verify、generateKey、importKey、exportKey模数≥2048位,哈希推荐SHA-256传统企业级API签名、老旧版本JWT签名验签
RSA-PSSsign、verify、generateKey、importKey、exportKeyRSA概率签名方案,密码学严谨度极高金融级或司法级等高安全要求的数字签名场景
HMACsign、verify、generateKey、importKey、exportKey基于哈希的密钥消息认证码前后端API请求防篡改哈希签名,如AWS签名机制
  • ECDSA 是Web场景下的首选签名方案,密钥体积小、验签速度快,非常适配高并发的现代Web服务。
  • RSA系列算法兼容性极强,适合需要对接老旧系统的业务场景,其中RSA-PSS的抗攻击能力远高于传统的RSASSA-PKCS1-v1_5。
  • HMAC实现成本低,是轻量API防篡改签名的最优选择。
  1. 单向哈希算法(Digest)表
算法名称支持的操作推荐搭配与技术参数典型应用场景
SHA-256digest无需密钥,单向不可逆文件完整性校验(秒传场景)、数据防篡改指纹生成
SHA-384digest无需密钥,单向不可逆,安全强度高于SHA-256高敏感数据校验、金融级业务的完整性验证
SHA-512digest无需密钥,单向不可逆,安全强度最高司法级、军工级等高安全要求场景的哈希校验

这三款同属SHA-2家族哈希算法,都具备单向不可逆的核心特性,无法从最终哈希值反推原始输入内容,仅用于生成数据唯一指纹,不能直接用于加解密还原原始数据。安全强度随输出位数提升而升高,同时计算性能也会小幅下降,普通业务场景优先选择SHA-256即可满足需求。

  1. 密钥派生算法(Key Derivation)表
算法名称支持的操作推荐搭配与技术参数典型应用场景
ECDHderiveKey、deriveBits、generateKey、importKey、exportKey椭圆曲线:Diffie-Hellman两个终端在不安全信道中,安全地协商出一致的对称密钥
PBKDF2deriveKey、deriveBits、importKey基于密码的密钥派生,推荐迭代≥100000次将用户输入的简短文明密码,转换为符合AES标准的高强度密钥
HKDFderiveKey、deriveBits、importKey基于HMAC的密钥扩展算法从一个基础主密钥,安全地扩展派生出多个子密钥
  • ECDH 是现代网络通信中密钥协商的核心方案,双方全程不需要直接传输最终的共享密钥,仅通过交换椭圆曲线公钥就能各自计算出完全一致的会话密钥,安全性极高。
  • PBKDF2 专门针对低熵的用户密码设计,通过高迭代次数大幅提升暴力破解的成本,是密码类密钥衍生的行业标准方案。
  • HKDF 面向已经具备高熵的主密钥材料,能高效地将单个主密钥拆分扩展为多个用途独立的子密钥,广泛应用于TLS等现代加密协议中。

6. Web Crypto API 不支持的主流算法

算法名称不支持原因替代实现方案
MD5已被密码学界证实存在碰撞漏洞,安全性完全失效引入crypto-js等第三方库实现或使用 SHA-256
SM2/SM3/SM4国密系列浏览器国际标准未原生内置国密算法引入sm-crypto等国密第三方库实现
ChaCha20-Poly1305/Ed25519目前尚未完全普及到所有主流浏览器的 crypto.subtle 中引入第三方加密库补充实现

7. 常用操作示例

以下是整理的 Web Crypto API (crypto.subtle) 最核心、最常用的四大标准代码示例(哈希生成、密钥生成、加密、解密)。需要特别提醒的是:所有 crypto.subtle 的操作都是异步的,都会返回一个 Promise,因此必须配合 async/await 使用。

7.1 对称加密AES-GCM示例

AES-GCM (Galois/Counter Mode) 是一种非常流行且强大的认证加密(AEAD, Authenticated Encryption with Associated Data) 模式。你可以把它理解为一种“二合一”的加密模式:它在高效加密数据的同时,还能确保数据没有被篡改。

AES-GCM 是兼具高性能和高安全性的认证加密模式,特别适合在 Web Crypto API 这类原生环境中使用。它的安全性高度依赖于每次加密都使用唯一且不可预测的 IV。遵循这一核心原则,并妥善管理密钥、认真验证认证标签,就能为你的 Web 应用构建起一道坚实的数据安全防线。

  1. 计算文本哈希摘要 (Digest):这是最简单的操作,常用于生成文件的 SHA-256。它不需要密钥。
/**
 * 计算字符串的 SHA-256 哈希值
 * @param {string} text - 输入的文本
 * @returns {Promise<string>} Hex 格式的哈希字符串
 */
async function generateHash(text) {
    // 1. 将文本编码为 Uint8Array 字节流
    const encoder = new TextEncoder();
    const data = encoder.encode(text);

    // 2. 使用 crypto.subtle.digest 生成哈希(返回 ArrayBuffer)
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);

    // 3. 将 ArrayBuffer 转换为常见的十六进制 (Hex) 字符串
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    return hashHex;
}

// 使用示例:
const sha256Result = await generateHash("Hello World");
  1. 生成对称加密密钥 (Generate Key):在加密数据之前,必须先通过 subtle 算法生成一个标准的加密密钥对象(CryptoKey)。这里以主流的 AES-GCM 算法为例:
/**
 * 生成一个用于 AES-GCM 加密的 256 位随机密钥
 * @returns {Promise<CryptoKey>} 返回 CryptoKey 对象
 */
async function generateAESKey() {
    return await crypto.subtle.generateKey(
        {
        name: 'AES-GCM',
        length: 256 // 密钥长度支持 128, 192, 或 256 位
        },
        false, // 是否允许该密钥被导出(如果设为 false,密钥无法脱离当前浏览器,安全性极高)
        ['encrypt', 'decrypt'] // 该密钥被允许用来做什么操作
    );
}
  1. 数据加密 Encrypt:使用生成的密钥和 AES-GCM 算法对敏感文本进行加密。为了安全,AES-GCM 要求每次加密必须配合一个随机的初始化向量 (IV)。
/**
 * 对文本进行 AES-GCM 加密
 * @param {string} text - 待加密的明文
 * @param {CryptoKey} key - 上一步生成的 CryptoKey 对象
 * @returns {Promise<{cipherText: ArrayBuffer, iv: Uint8Array}>} 返回密文和IV
 */
async function encryptText(text, key) {
    const encoder = new TextEncoder();
    const encodedText = encoder.encode(text);

    // AES-GCM 推荐使用 12位(96位)的高安全伪随机数作为 IV
    // 注意:这里使用的是外部同步方法 getRandomValues 生成随机数
    const iv = crypto.getRandomValues(new Uint8Array(12));

    // 执行加密
    const cipherText = await crypto.subtle.encrypt(
        {
        name: 'AES-GCM',
        iv: iv // 加密参数必须包含 IV
        },
        key, // 密钥
        encodedText // 明文数据
    );

    // 需要同时把密文和 IV 返回,因为解密时必须提供一模一样的 IV
    return { cipherText, iv };
}
  1. 数据解密 Decrypt:传入加密时使用的 CryptoKey、IV 和密文数组,将其还原为人类可读的明文字符串。
/**
 * 对 AES-GCM 密文进行解密
 * @param {ArrayBuffer} cipherText - 密文数据
 * @param {CryptoKey} key - 加密时使用的同一个密钥
 * @param {Uint8Array} iv - 加密时对应的随机 IV
 * @returns {Promise<string>} 解密后的明文字符串
 */
async function decryptText(cipherText, key, iv) {
    // 执行解密
    const decryptedBuffer = await crypto.subtle.decrypt(
        {
        name: 'AES-GCM',
        iv: iv // 必须传入与加密时完全相同的 IV
        },
        key,
        cipherText
    );

    // 将 ArrayBuffer 字节解码还原为字符串
    const decoder = new TextDecoder();
    return decoder.decode(decryptedBuffer);
}
如果要在项目中保存或传输生成的密钥和密文,通常需要通过 crypto.subtle.exportKey('raw', key) 将密钥导出成字节,再用 btoa() 或 Base64 方案 转换成可传输的字符串格式。

7.2 非对称加密RSA-OAEP示例

非对称加密使用公钥加密、私钥解密,常用于非对称场景或加密对称密钥(如密钥交换)。这里使用最安全的 RSA-OAEP 算法。

  1. 生成 RSA 密钥对
/**
 * 生成用于 RSA-OAEP 加解密的 2048 位密钥对
 * @returns {Promise<CryptoKeyPair>} 返回包含公钥和私钥的对象
 */
async function generateRSAKeyPair() {
    return await crypto.subtle.generateKey(
        {
            name: "RSA-OAEP",
            modulusLength: 2048, // 密钥长度,2048 位是目前的标准安全线
            publicExponent: new Uint8Array([1, 0, 1]), // 等于 65537,固定的密码学标准常数
            hash: "SHA-256", // 内部使用的哈希算法
        },
        true, // 是否允许导出
        ["encrypt", "decrypt"] // 用途分配
    );
}
  1. 使用公钥加密
/**
 * 使用 RSA 公钥加密文本
 */
async function rsaEncrypt(text, publicKey) {
    const encoder = new TextEncoder();
    const data = encoder.encode(text);

    // 返回 ArrayBuffer 格式的密文
    return await crypto.subtle.encrypt(
        { name: "RSA-OAEP" },
        publicKey,
        data
    );
}
  1. 使用私钥解密
/**
 * 使用 RSA 私钥解密文本
 */
async function rsaDecrypt(cipherText, privateKey) {
    const decryptedBuffer = await crypto.subtle.decrypt(
        { name: "RSA-OAEP" },
        privateKey,
        cipherText
    );

    const decoder = new TextDecoder();
    return decoder.decode(decryptedBuffer);
}

7.3 数字签名与验证 (RSASSA-PKCS1-v1_5)

数字签名常用于防止数据被篡改并确认发送者身份。其逻辑是:私钥签名、公钥验证。这里使用最通用的 RSASSA-PKCS1-v1_5 算法。

  1. 生成数字签名的 RSA 密钥对
/**
 * 生成用于数字签名的 RSA 密钥对
 */
async function generateSignKeyPair() {
    return await crypto.subtle.generateKey(
        {
        name: "RSASSA-PKCS1-v1_5",
        modulusLength: 2048,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-256",
        },
        true,
        ["sign", "verify"] // 注意:这里的用途是签名和验证
    );
}
  1. 使用私钥对数据生成签名

/**
 * 使用私钥对数据生成签名
 * @returns {Promise<ArrayBuffer>} 返回签名摘要
 */
async function signData(text, privateKey) {
    const encoder = new TextEncoder();
    const data = encoder.encode(text);

    return await crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        data
    );
}
  1. 公钥验证签名是否有效

/**
 * 使用公钥验证签名是否有效
 * @returns {Promise<boolean>} 验证成功返回 true,被篡改则返回 false
 */
async function verifyData(text, signature, publicKey) {
    const encoder = new TextEncoder();
    const data = encoder.encode(text);

    return await crypto.subtle.verify(
        { name: "RSASSA-PKCS1-v1_5" },
        publicKey,
        signature, // 之前生成的签名 ArrayBuffer
        data // 原始数据
    );
}

7.4 密钥的导入与导出 (Import / Export)

在实际业务中,密钥不能只存在于浏览器内存中,我们需要将其导出为字符串传给后端,或者将后端的字符串密钥导入为浏览器可识别的 CryptoKey 对象。

Web Crypto 支持多种格式,最常用的是 JWK (JSON Web Key) 格式(易读、跨平台)和 raw/spki/pkcs8(二进制字节流)。

  1. 导出密钥为 JSON (JWK 格式)
/**
 * 将 CryptoKey 密钥对象导出为标准的 JSON 对象 (JWK)
 * @param {CryptoKey} key 
 * @returns {Promise<Object>} 可直接用于传输或 JSON.stringify 的对象
 */
async function exportKeyToJWK(key) {
    return await crypto.subtle.exportKey("jwk", key);
}
  1. 将 JSON 导入回密钥对象
/**
 * 将 JWK 格式的 JSON 对象导入还原为可用的 AES-GCM 密钥
 * @param {Object} jwkObject 
 * @returns {Promise<CryptoKey>}
 */
async function importKeyFromJWK(jwkObject) {
    return await crypto.subtle.importKey(
        "jwk",
        jwkObject,
        { name: "AES-GCM" }, // 必须指定对应的算法
        true, // 是否允许再次导出
        ["encrypt", "decrypt"] // 该密钥允许的用途
    );
}

8 避坑指南

  1. 陷阱一:IV 重复使用
  • 问题: 在 AES-GCM 等对称加密中,重复使用相同的初始化向量(IV)会导致密钥流重复,严重破坏加密安全性。
  • 正确做法:
    • 每次加密都使用 crypto.getRandomValues() 生成新的随机 IV
    • AES-GCM 模式推荐使用 12 字节(非 16 字节!)的 IV
    • 同一密钥下 IV 重复概率应低于 2⁻³²
    • IV 不需要保密,但必须唯一——记得将 IV 与密文一起存储
  1. 陷阱二:密钥管理不当
  • 常见错误:
    • 将密钥存储在 localStorage 或 Cookie 中
    • 使用不安全的密钥传输方式
    • 未设置适当的密钥使用限制
  • 安全建议:
    • 敏感会话密钥存储在 Web Worker 内存中
    • 需要持久化的密钥使用 IndexedDB
    • 使用 exportKey 和 importKey 安全地传输公钥
  1. 陷阱三:算法选择失误
  • 不要使用 SHA-1 或 MD5 进行哈希
  • 不要使用 ECB 模式的 AES 加密
  • RSA 密钥长度至少 2048 位

9 最佳实践

  1. 使用方式选型指南推荐

自 Node.js 19+ 起,Web Crypto API 已成为内置标准,可通过以下方式访问:

// 方式一:使用 globalThis
const { subtle } = globalThis.crypto;

// 方式二:使用 Node.js 模块
const { webcrypto } = require('node:crypto');
const { subtle } = webcrypto;

这两种方式虽然最终都能让你获取到 subtle 接口,但它们在设计哲学、模块机制、环境通用性以及底层依赖上有着本质的区别。

对比维度方式一:globalThis.crypto方式二:require('node:crypto')
本质定位Web标准API(与浏览器对齐)Node.js原生模块API(特定服务端)
引入方式零依赖,直接全局访问需要显式通过require引入模块
跨端通用性极高(同构JavaScript)极低(只能在Node.js运行)
底层实现浏览器内核(前端)/ Node V8底层封装(后端)强依赖Node.js内置的OpenSSL库
执行上下文必须在安全上下文(前端限制)服务端环境,无任何安全上下文限制

推荐使用"globalThis.crypto"的场景: 正在编写纯前端项目、前后端全栈通用工具库(Isomorphic Lib),或者代码需要部署在 Cloudflare Workers、Vercel Edge 等现代边缘计算架构中。

推荐使用"require('node:crypto')"的场景: 正在编写纯 Node.js 服务端项目(如大型后端服务、CLI 工具),且除了 Web Crypto 之外,还需要用到 Node.js 特有的高级安全特性(如流式加密大文件、硬件安全模块 HSM 交互等)。

  1. 使用细节建议

Web Crypto API 是浏览器原生加密的黄金标准。它以零依赖、高性能、高安全性的优势,正在逐步取代各种第三方加密库。然而,正如其名 "subtle" 所警示的,加密是一个极其专业的领域,细微的错误就可能导致整个系统安全性完全失效。在使用 Web Crypto API 时:

  • 始终在 HTTPS 环境下使用
  • 选择正确的算法和参数(如 AES-GCM + 12 字节 IV)
  • 妥善管理密钥的生命周期

10 加解密模板工具

以下为您编写的这一套 AES-GCM 高级对称加密与解密工具函数。它做到了100% 零依赖、纯原生,通过 globalThis.crypto 实现完美同构。你不需要进行任何环境判断,同一段代码可以无缝运行在浏览器(安全上下文)、Node.js(19+)、Deno、Bun 以及边缘计算(如 Cloudflare Workers)中。

为了解决数据传输与存储的问题,工具函数内部自动处理了 ArrayBuffer 与 Base64 字符串之间的相互转换。可以直接复制使用,代码如下:

crypto.ts
/**
 * 完美的同构 Web Crypto 安全加解密工具库 (TypeScript 箭头函数版本)
 * 适用环境:Browser (HTTPS), Node.js 19+, Deno, Bun, Cloudflare Workers
 */

// 1. 获取全局同构的 subtle 对象
const cryptoProvider = globalThis.crypto
if (!cryptoProvider) {
    throw new Error('当前运行环境不支持 Web Crypto API。请确保在 HTTPS、localhost 或现代 Node.js 19+ 环境下运行。')
}
const subtle: SubtleCrypto = cryptoProvider.subtle

/**
 * 辅助函数:将 ArrayBuffer 转换为可传输的 Base64 字符串
 * 使用现代批处理写法apply,100% 消除严格模式下的 undefined 类型报错
 */
const bufferToBase64 = (buffer: ArrayBuffer): string => {
    const bytes = new Uint8Array(buffer)
    const binary = String.fromCharCode.apply(null, bytes as unknown as number[])
    return btoa(binary)
}

/**
 * 辅助函数:将 Base64 字符串还原为 ArrayBuffer
 * 使用 Array.from 映射,确保严格类型安全
 */
const base64ToBuffer = (base64: string): ArrayBuffer => {
    const binaryString = atob(base64)
    const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0))
    return bytes.buffer
}

/**
 * [同构] 生成 AES-GCM 256位加密密钥
 */
export const generateAESKey = async (): Promise<CryptoKey> => {
    return await subtle.generateKey(
        { name: 'AES-GCM', length: 256 },
        true, // 允许导出
        ['encrypt', 'decrypt']
    )
}

/**
 * [同构] 导出 CryptoKey 密钥为可传输的 Base64 字符串
 * @param cryptoKey - 浏览器或服务端生成的 CryptoKey 实体
 */
export const exportKeyToBase64 = async (cryptoKey: CryptoKey): Promise<string> => {
    const exported = await subtle.exportKey('raw', cryptoKey)
    return bufferToBase64(exported)
}

/**
 * [同构] 从 Base64 字符串导入还原为 CryptoKey 密钥对象
 * @param base64Key - Base64 格式的密钥字符串
 */
export const importKeyFromBase64 = async (base64Key: string): Promise<CryptoKey> => {
    const rawKeyBuffer = base64ToBuffer(base64Key)
    return await subtle.importKey('raw', rawKeyBuffer, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt'])
}

/**
 * [同构] 使用 AES-GCM 算法加密字符串文本
 * @param text - 待加密的明文文本
 * @param key - CryptoKey 密钥对象
 * @returns 返回打包了 "IV" 和 "密文" 的统一 Base64 字符串
 */
export const encryptText = async (text: string, key: CryptoKey): Promise<string> => {
    const encoder = new TextEncoder()
    const encodedText: Uint8Array<ArrayBuffer> = encoder.encode(text)
    // 生成 12 字节的密码学安全伪随机数作为 IV (AES-GCM 标准推荐)
    const iv: Uint8Array<ArrayBuffer> = cryptoProvider.getRandomValues(new Uint8Array(12))
    // 执行加密,返回密文 ArrayBuffer
    const cipherBuffer: ArrayBuffer = await subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, encodedText)
    // 将 IV(12字节)和密文拼接,方便单字符串传输
    const combined = new Uint8Array(iv.length + cipherBuffer.byteLength)
    combined.set(iv, 0)
    combined.set(new Uint8Array(cipherBuffer), iv.length)
    return bufferToBase64(combined.buffer)
}

/**
 * [同构] 使用 AES-GCM 算法解密文本
 * @param combinedBase64 - 由 encryptText 生成的统一加密 Base64 字符串(内部含 IV)
 * @param key - 对应的 CryptoKey 密钥对象
 */
export const decryptText = async (combinedBase64: string, key: CryptoKey): Promise<string> => {
    const combinedBuffer = base64ToBuffer(combinedBase64)
    const combinedBytes = new Uint8Array(combinedBuffer)
    // 从头部切出前 12 字节作为 IV
    const iv: Uint8Array<ArrayBuffer> = combinedBytes.slice(0, 12)
    // 剩下的部分作为真正的密文
    const cipherText: Uint8Array<ArrayBuffer> = combinedBytes.slice(12)
    // 执行解密
    const decryptedBuffer: ArrayBuffer = await subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, cipherText.buffer)
    const decoder = new TextDecoder()
    return decoder.decode(decryptedBuffer)
}

TS 测试脚本

如果你在 Vite/Nuxt/Next.js 项目或者使用了 ts-node / bun / deno 的后端环境中,可以直接引入并运行这段强类型的测试代码:

test-crypto.ts
import { decryptText, encryptText, exportKeyToBase64, generateAESKey, importKeyFromBase64 } from 'crypto'
;(async () => {
    try {
        const rawData: string = '前后端全平台无缝传输的敏感数据 1234-abcd-TS-VERSION'
        console.log('🔒 原始待加解密明文:', rawData)

        // ==========================================
        // 阶段一:在 A 端(例如 Node.js 后端)生成密钥并加密
        // ==========================================
        const originalKey: CryptoKey = await generateAESKey()

        // 导出密钥为 Base64 字符串
        const transportableKeyStr: string = await exportKeyToBase64(originalKey)
        console.log('🔑 [传输中] 密钥已安全序列化为 Base64:', transportableKeyStr)

        // 对数据进行加密
        const secureCipherText: string = await encryptText(rawData, originalKey)
        console.log('📦 [传输中] 密文已安全打包(含IV)为 Base64:', secureCipherText)

        // ==========================================
        // 阶段二:在 B 端(例如现代 Web 浏览器前端)收到数据解密
        // ==========================================
        console.log('\n--- 数据已通过网络无缝抵达另一端 ---')

        // 1. 恢复强类型的 CryptoKey 实体
        const receivedKey: CryptoKey = await importKeyFromBase64(transportableKeyStr)

        // 2. 还原数据内容
        const decryptedOutput: string = await decryptText(secureCipherText, receivedKey)
        console.log('🔓 [还原成功] 解密后的最终明文结果:', decryptedOutput)
    } catch (error) {
        console.error('❌ 密码学同构解密链路失败,错误原因: ', error)
    }
})()
最后更新时间: 2026-08-26 10:46:16
edgeone - 小紫念沁的博客
2022-2026 小紫念沁 版权所有