JS生成加密安全的随机数的 API—crypto.getRandomValues()

149 阅读7分钟

getRandomValues 是 JavaScript 中用于生成加密安全的随机数的 API,属于 Crypto 接口(Web Crypto API 的一部分),相比传统的 Math.random(),它能生成更安全、不可预测的随机数,适用于密码学场景(如生成密钥、令牌、验证码等)。

一、核心特点与优势

1. 对比 Math.random() 的优势

特性Math.random()crypto.getRandomValues()
安全性弱(伪随机,可预测,基于种子)强(加密安全,不可预测,基于硬件 / 系统随机源)
适用场景普通随机场景(如随机排序、抽奖)安全敏感场景(如生成密钥、令牌、验证码)
输出范围[0, 1) 浮点数整数(按指定类型和范围生成)

2. 关键特性

  • 加密安全:符合密码学安全标准(如 NIST 规范),随机数不可预测、不可重现,能抵御暴力破解。
  • 多类型支持:可生成 8 位 / 16 位 / 32 位整数(有符号 / 无符号)、64 位浮点数等。
  • 浏览器原生支持:现代浏览器(Chrome 37+、Firefox 34+、Safari 7.1+、Edge 12+)均支持,无需额外库。

二、基本语法

// 语法:crypto.getRandomValues(typedArray)
const typedArray = new TypedArray(length);
crypto.getRandomValues(typedArray);

改造一下

Math.randomValue = function () { 
    return self.crypto.getRandomValues(new Uint32Array(1))[0]; 
};
  • 参数typedArray - 一个定型数组(TypedArray) (如 Uint8ArrayUint32Array 等),用于存储生成的随机数。

  • 返回值:修改后的定型数组(随机数会直接写入输入的数组,无需额外接收返回值)。

  • 支持的定型数组类型

    • 整数类型:Uint8Array(8 位无符号)、Int8Array(8 位有符号)、Uint16Array(16 位无符号)、Int16Array(16 位有符号)、Uint32Array(32 位无符号)、Int32Array(32 位有符号)。
    • 浮点数类型:Float32ArrayFloat64Array(生成 [0, 1) 范围的浮点数)。

image.png

三、常用示例

1. 生成指定范围的随机整数(最常用)

需求:生成 [min, max] 之间的加密安全随机整数(包含边界)。

/**
 * 生成 [min, max] 范围内的加密安全随机整数
 * @param {number} min - 最小值(整数)
 * @param {number} max - 最大值(整数)
 * @returns {number} 随机整数
 */
function getSecureRandomInt(min, max) {
  // 校验参数:确保 min <= max 且为整数
  if (!Number.isInteger(min) || !Number.isInteger(max) || min > max) {
    throw new Error('min 和 max 必须是整数,且 min <= max');
  }

  // 计算范围大小
  const range = max - min + 1;

  // 若范围为 1,直接返回 min(无需生成随机数)
  if (range === 1) return min;

  // 选择合适的定型数组类型(范围越小,类型越节省内存)
  let typedArray;
  if (range <= 256) {
    typedArray = new Uint8Array(1); // 8位无符号:0-255
  } else if (range <= 65536) {
    typedArray = new Uint16Array(1); // 16位无符号:0-65535
  } else {
    typedArray = new Uint32Array(1); // 32位无符号:0-4294967295
  }

  // 生成随机数(循环确保结果在范围内,避免偏倚)
  let randomValue;
  do {
    crypto.getRandomValues(typedArray);
    randomValue = typedArray[0];
  } while (randomValue >= Math.floor(2 ** (typedArray.BYTES_PER_ELEMENT * 8) / range) * range);

  // 映射到目标范围
  return min + (randomValue % range);
}

// 测试:生成 1-100 之间的随机整数
console.log(getSecureRandomInt(1, 100)); // 如 37、89 等(每次不同)

// 测试:生成 0-9 之间的随机数(验证码常用)
console.log(getSecureRandomInt(0, 9)); // 如 5、2 等
  • 核心逻辑:通过循环过滤掉超出「范围整数倍」的随机数,避免因定型数组最大值不是范围的整数倍导致的概率偏倚(如用 8 位随机数生成 0-10 时,0-5 出现概率略高)。

2. 生成加密安全的随机字符串(如令牌、验证码)

需求:生成 16 位由字母 + 数字组成的随机令牌。

/**
 * 生成指定长度的加密安全随机字符串(字母+数字)
 * @param {number} length - 字符串长度
 * @returns {string} 随机字符串
 */
function getSecureRandomString(length = 16) {
  if (!Number.isInteger(length) || length < 1) {
    throw new Error('长度必须是大于 0 的整数');
  }

  // 字符池:包含大小写字母和数字(共 62 个字符)
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const charsetLength = charset.length;

  // 生成足够的随机整数(每个整数对应一个字符)
  const typedArray = new Uint32Array(length);
  crypto.getRandomValues(typedArray);

  // 映射随机数到字符池
  let result = '';
  for (let i = 0; i < length; i++) {
    const randomIndex = typedArray[i] % charsetLength;
    result += charset[randomIndex];
  }

  return result;
}

// 测试:生成 16 位随机令牌
console.log(getSecureRandomString(16)); // 如 "x7Z9k2PqR3sT8vU1"(每次不同)

// 测试:生成 6 位数字验证码
function getSecureVerifyCode(length = 6) {
  const charset = '0123456789';
  const typedArray = new Uint32Array(length);
  crypto.getRandomValues(typedArray);
  return Array.from(typedArray, num => charset[num % 10]).join('');
}
console.log(getSecureVerifyCode()); // 如 "385271"(每次不同)

3. 生成 [0, 1) 范围的加密安全浮点数

类似 Math.random(),但更安全:

function getSecureRandomFloat() {
  const typedArray = new Float64Array(1);
  crypto.getRandomValues(typedArray);
  return typedArray[0]; // 范围 [0, 1)
}

console.log(getSecureRandomFloat()); // 如 0.3456789012345678(加密安全)

四、注意事项

  1. 参数必须是定型数组:不能直接传递普通数组(如 []),必须使用 Uint8Array 等定型数组,否则会抛出 TypeError
  2. 避免范围偏倚:生成指定范围整数时,若定型数组的最大值不是「范围大小」的整数倍,需通过循环过滤超出部分(如示例 1 中的 do-while 逻辑),否则会导致部分数值出现概率更高。
  3. 浏览器兼容性:现代浏览器均支持,但 IE 11 及以下不支持(需降级为 Math.random() 或使用第三方库)。
  4. 不能用于大量数据getRandomValues 有性能限制(单次生成的数组长度不宜过大,通常建议不超过 65536 个元素),大量随机数生成需分批处理。

五、适用场景

  • 密码学相关:生成加密密钥、会话令牌(Session ID)、JWT 密钥、加密盐(Salt)。
  • 安全验证:生成手机验证码、邮箱验证码、图形验证码。
  • 敏感随机场景:抽奖(避免作弊)、随机分配敏感资源(如红包金额)。

普通非敏感场景(如随机排序、随机展示内容)仍可使用 Math.random(),但涉及安全的场景必须使用 getRandomValues

总结

crypto.getRandomValues() 是生成加密安全随机数的标准 API,核心优势是 “不可预测、无偏倚”,适用于所有安全敏感场景。使用时需注意:

  • 必须传递定型数组存储结果;
  • 生成指定范围整数时需避免偏倚;
  • 现代浏览器支持,IE 需降级。

日常开发中,若需 “安全随机”,优先使用该 API 而非 Math.random()

Math.random的底层实现原理

Math.random() 函数返回一个范围0-1的伪随机浮点数,其在 V8 中的实现原理是这样的:

为了保证足够的性能,Math.random() 随机数并不是实时生成的,而是直接生成一组随机数(64个),并放在缓存中。

当这一组随机数取完之后再重新生成一批,放在缓存中。

由于 Math.random() 的底层算法是公开的(xorshift128+ 算法),V8 源码可见,因此,是可以使用其他语言模拟的,这就导致,如果攻击者知道了当前随机生成器的状态,那就可以知道缓存中的所有随机数,那就很容易匹配与破解。

例如抽奖活动,使用 Math.random() 进行随机,那么就可以估算出一段时间内所有的中奖结果,从而带来非常严重且致命的损失。此时应该使用 getRandomValues() 方法。

getRandomValues() 方法的底层实现是没有缓存的,随机数都是实时生成的,因此,性能上是要比 Math.random() 差的,因此,如果是高并发的场景,同时随机数仅仅是用做随机,与安全和金钱不相关,请使用 Math.random() 而不是 getRandomValues()

就 Web 前端而言,必须要使用 getRandomValues() 方法的场景很少,不过由于纯前端几乎不存在所谓的高并发,因此,你使用 getRandomValues() 方法也是可以的,有装逼的作用。