当我们需要一个随机数时,Math.random() 几乎是所有人的第一反应。它简单、直接,一行代码就能得到一个 0 到 1 之间的浮点数。然而,这个信手拈来的函数,却有着致命的缺陷。
Math.random() 的“原罪”:它是可预测的
Math.random() 生成的数字并非真正的随机,而是伪随机。什么是伪随机?它是由一个确定的算法,根据一个初始值(称为“种子”)计算出来的一系列数字。这个算法本身是公开的,这意味着,如果你知道了初始的“种子”,你就能完全预测出接下来生成的每一个“随机数”。在早期的浏览器中,这个“种子”甚至可能只是简单的时间戳,使得预测变得非常容易。虽然现代浏览器已经改进了种子的生成方式,使其更难被猜测,但 Math.random() 的核心机制并没有改变。ECMAScript 规范本身不要求 Math.random() 必须是密码学安全的。
更安全的替代方案:crypto.getRandomValues()
window.crypto 是浏览器提供的一套用于密码学操作的 API,而 crypto.getRandomValues() 就是其中的一员。它是一个密码学安全伪随机数生成器 (CSPRNG)。与 Math.random() 不同,crypto.getRandomValues() 的设计目标就是提供密码学级别的安全性。
它是如何做到“真正随机”的?
它直接从操作系统底层获取高质量的“熵 (Entropy)”。这些熵的来源是不可预测的物理事件,例如:
1.鼠标移动的精确时机和轨迹
2.键盘输入的时机
3.硬件设备产生的微小噪声
4.网络数据包的到达时间
操作系统将这些不可预测的事件混合成一个“熵池”,crypto.getRandomValues() 正是从这个池中获取随机性,使其生成的数值在统计学上是真正不可预测的。
如何使用 crypto.getRandomValues()?
它的用法与 Math.random() 有所不同。它不是直接返回一个数字,而是用于填充一个类型化数组 (Typed Array),如 Uint8Array 或 Uint32Array。
基础用法:
// 堆代码 duidaima.com
// 创建一个包含 10 个字节的数组
const randomBytes = new Uint8Array(10);
// 用密码学安全的随机值填充它
crypto.getRandomValues(randomBytes);
console.log(randomBytes); // 输出: Uint8Array(10) [185, 20, 248, 119, ...]
这看起来似乎没那么直观,但别担心,我们可以轻松地将它封装成我们习惯使用的函数。
替代 Math.random() 的函数:
我们可以生成一个 32 位无符号整数,然后将其转换为 0 到 1 之间的浮点数。
生成范围内安全随机整数的函数(常用):
function secureRandomInt(min, max) {
const range = max - min + 1;
// 创建一个足够大的随机数,以减少模偏差
const randomValue = new Uint32Array(1);
crypto.getRandomValues(randomValue);
return min + (randomValue[0] % range);
}
console.log(secureRandomInt(1, 6)); // 模拟安全的骰子
console.log(secureRandomInt(1000, 9999)); // 生成一个安全的 4 位验证码
Math.random() 适用于那些不涉及安全或公平性的应用场景,例如:
1.生成随机的粒子效果、模拟下雨或下雪
2.创作随机的图案和视觉效果
3.需要玩家通过分享种子来玩到完全相同的游戏关卡
4.当需要真随机时,请选择 crypto.getRandomValues(),目前早已兼容各现代浏览器(IE 除外)。