广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何正确使用 window.crypto 生成安全随机数并调用 Web Crypto API?

浏览器的 window 对象上挂着一个不太起眼但很重要的属性:window.crypto。它不代表某个具体加密算法,而是整个 Web Crypto API 的入口。日常开发里只要涉及令牌生成、验证码、盐值、nonce、签名或者本地数据加解密,都应当优先考虑这里提供的能力,而不是自己手写随机算法或直接使用 Math.random。理解清楚这个入口对象的成员和限制,可以少走很多弯路。

如何正确使用 window.crypto 生成安全随机数并调用 Web Crypto API?

window.crypto 的定位与安全上下文

window.crypto 是浏览器给 JavaScript 暴露的只读属性,它的类型是 Crypto 接口。接口本身没有构造方法,只能通过 window 对象访问。当前主流浏览器中,这个对象主要包含三个成员:getRandomValues、subtle 和较新的 randomUUID。getRandomValues 负责生成加密安全随机数,subtle 是 SubtleCrypto 实例,提供摘要、签名、加解密等密码学原语,randomUUID 可以快速生成符合 RFC 4122 的 UUID。三个成员分工明确,覆盖了前端最常见的安全基础需求。

这里有一个非常容易踩到的点:并非只要能看到 window.crypto,就代表所有能力都可用。getRandomValues 在设计上被允许在非安全上下文里使用,但 crypto.subtle 和 crypto.randomUUID 要求页面处于安全上下文。安全上下文通常指 HTTPS 或 localhost。也就是说,如果站点部署在普通 HTTP 域名上,window.crypto.subtle 很可能是 undefined,调用 subtle.digest 会直接抛出类型错误。很多报错最终定位到这里,才发现测试环境用了 HTTP 而不是 HTTPS。

判断当前环境是否支持最佳实践,可以先用下面的代码做显式检测。不要只判断 window.crypto 是否存在。

if (window.isSecureContext) {
  if (window.crypto) {
    if (window.crypto.subtle) {
      console.log('支持 SubtleCrypto');
    } else {
      console.log('当前环境不支持 subtle,请检查是否使用 HTTPS 或 localhost');
    }
  }
}

这段代码没有使用与逻辑符叠加判断,而是逐层确认,排查问题时会更清楚。对于本地开发,localhost 会被浏览器视为安全上下文,但局域网 IP 地址访问通常不是。如果真机测试时碰到 subtle 不可用,可以换成 localhost 或给测试环境配置自签名证书。

用 getRandomValues 生成安全随机数

前端生成令牌、验证码、邀请码时,很多人习惯直接写 Math.random().toString(36).slice(2)。如果这些值只用于 UI 展示,问题不大;但一旦进入安全链路,比如找回密码链接、CSRF Token、抽奖随机数或临时 nonce,Math.random 的弱点就会被放大。Math.random 使用伪随机数生成器,输出虽然看起来随机,但内部状态可能被推测。攻击者如果拿到了足够多的输出样本,就有机会推算出下一个值,进而伪造令牌或绕过校验。

window.crypto.getRandomValues 则完全不同。它接收一个类型数组作为参数,并将加密安全随机值写入该数组。随机源来自浏览器或操作系统底层的密码学安全随机数生成器,熵通常来自硬件噪声、系统调度事件等,输出被设计为不可预测。下面的函数可以生成一个十六进制安全令牌:

function generateSafeToken(length) {
  var byteLength = length || 32;
  var bytes = new Uint8Array(byteLength);
  window.crypto.getRandomValues(bytes);
  var result = '';
  for (var i = 0; i < bytes.length; i++) {
    result += ('0' + bytes[i].toString(16)).slice(-2);
  }
  return result;
}
var token = generateSafeToken(32);
console.log(token);

上面的代码先把 32 字节的随机数拿到手,再逐字节转成两位十六进制字符串,最终形成一个 64 位长度的随机令牌。如果需要生成数字验证码,可以传入 Uint32Array,把结果对某个范围取余;但取余会引入轻微偏差,对高安全场景应使用拒绝采样。若只是生成 UUID,现代浏览器可以直接调用 crypto.randomUUID(),注意它同样要求安全上下文。

另一个要注意的是配额限制。getRandomValues 单次最多能填充 65536 字节,超出后浏览器会抛出 QuotaExceededError。如果业务里要生成大量随机数据,比如做批量加密 IV 或临时密钥,可以在循环里分批调用。对绝大多数 Web 场景来说,这个上限已经足够,但了解它能避免偶发的异常。

SubtleCrypto 常用密码学操作

crypto.subtle 返回 SubtleCrypto 实例。这里的 subtle 可以理解为“精细的、底层的”,它不是高层业务加密工具,而是一组密码学原语。常用方法包括 digest、generateKey、encrypt、decrypt、sign、verify、deriveKey 等。这些方法几乎都返回 Promise,所以要在 async 函数里配合 await 使用,或者用 .then 处理。它也要求页面处于安全上下文,否则方法根本不存在。

先看最简单的摘要计算。SHA-256 属于常见摘要算法,可以用于生成内容指纹或校验数据完整性。下面代码把字符串转成 UTF-8 字节,再交给 digest 计算:

async function sha256Hex(text) {
  var encoder = new TextEncoder();
  var data = encoder.encode(text);
  var digestBuffer = await window.crypto.subtle.digest('SHA-256', data);
  var hashArray = Array.from(new Uint8Array(digestBuffer));
  var hashHex = hashArray.map(function (byte) {
    return byte.toString(16).padStart(2, '0');
  }).join('');
  return hashHex;
}

摘要结果是 ArrayBuffer,读取时需要先包一层 Uint8Array。TextEncoder 和 TextDecoder 分别负责字符串到字节、字节到字符串的转换,编码默认是 UTF-8。摘要算法不可逆,但它不是加密,不能用来隐藏信息,只适合校验。

如果要在前端加密数据,再写入 localStorage 或 sessionStorage,AES-GCM 是比较稳妥的选择。它同时提供机密性和完整性保护,IV 每次都要重新生成,不能重复使用。下面是一组生成密钥、加密、解密的示例:

async function createAesKey() {
  return window.crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}
async function encryptText(key, plainText) {
  var encoder = new TextEncoder();
  var iv = window.crypto.getRandomValues(new Uint8Array(12));
  var encoded = encoder.encode(plainText);
  var cipherBuffer = await window.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: iv },
    key,
    encoded
  );
  return {
    iv: iv,
    cipherText: new Uint8Array(cipherBuffer)
  };
}
async function decryptText(key, iv, cipherText) {
  var cipherBuffer = new Uint8Array(cipherText);
  var plainBuffer = await window.crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: iv },
    key,
    cipherBuffer
  );
  var decoder = new TextDecoder();
  return decoder.decode(plainBuffer);
}

代码中 generateKey 的第三个参数是 false,表示密钥不可导出。这样即使拿到 CryptoKey 对象,也无法通过 exportKey 导出原始密钥材料。加密时 IV 长度选择 12 字节,这是 AES-GCM 的推荐长度。虽然这个示例演示了前端加解密能力,但它不能替代 HTTPS,因为密钥仍需安全传递和保存。

常见问题与注意事项

最容易出错的是安全上下文。很多业务代码在 HTTPS 线上环境运行正常,一到普通 HTTP 的测试域名就报错。定位时先打印 window.isSecureContext 和 window.crypto.subtle,能少走很多弯路。除了 localhost 被浏览器视为安全上下文外,任何通过 HTTP 访问的域名或 IP 都可能缺失 semantic 能力。

其次是随机数 API 的用法。getRandomValues 只接受整数类型数组,传 Float32Array 会抛出类型错误。它也不返回新数组,而是原地填充传入的数组,所以调用前需要先 new 出足够长度的视图。对于高并发场景,不要重复使用同一个数组对象被其他逻辑篡改,最好在每次使用时创建新的 Uint8Array。

关于“前端加密”的误区也需要警惕。用 Web Crypto API 对密码先加密再发送,并不能替代 HTTPS。攻击者如果能在网络上劫持页面,也能篡改前端脚本,把密码在加密前发送出去。Web Crypto API 的价值更多体现在生成不可预测的随机值、对请求做签名、在本地保护临时数据、配合服务端完成端到端场景等。真正涉及传输安全时,仍然必须依赖 HTTPS。

兼容性方面,AES-GCM、HMAC、SHA-256、ECDSA 等算法在主流浏览器中支持较好,可以放心使用。而一些新算法在 Web Crypto API 中可能还未落地,或只在少数浏览器可用。上线前应确认目标浏览器是否支持对应的算法名称和参数。如果要处理文本,注意统一使用 UTF-8 编码,避免前后端编码不一致导致摘要或签名校验失败。

广告位 · 文章底部(后台-模块管理替换此区域)