JS 安全随机整数:Math.random() 还是 crypto.getRandomValues()

Math.random() 的问题

const randomInt = (num, offset) => Math.floor(Math.random() * num) + offset;

Math.random() 生成的是伪随机数,不适合安全场景:

  • 某些 JS 引擎实现可预测
  • 不能用于:token、验证码、session ID、抽奖、游戏反作弊

用于 UI 动画、随机颜色、普通业务逻辑完全没问题,够快够简单。

安全随机整数

浏览器

export const randomInt = (min, max) => {
    const range = max - min + 1;
    const bytes = new Uint32Array(1);
    crypto.getRandomValues(bytes);
    return min + (bytes[0] % range);
};

有轻微的 modulo bias(取模偏差)——当 range 不能整除 0xFFFFFFFF 时,某些值出现概率略高。对大多数业务足够,抽奖/密码学场景用下面的拒绝采样版:

export const randomInt = (min, max) => {
    const range = max - min + 1;
    if (range <= 0 || range > 0xFFFFFFFF) throw new Error("invalid range");

    const maxUint = 0xFFFFFFFF;
    const limit = maxUint - (maxUint % range);  // 去掉尾部偏差区间
    const bytes = new Uint32Array(1);

    let rand;
    do {
        crypto.getRandomValues(bytes);
        rand = bytes[0];
    } while (rand >= limit);  // 拒绝偏差区间内的值

    return min + (rand % range);
};

平均循环次数 < 2,性能影响可以忽略。

Node.js

import crypto from "crypto";

export const randomInt = (min, max) => {
    return crypto.randomInt(min, max + 1);  // 内部已处理 modulo bias
};

Node.js 的 crypto.randomInt 直接用就行,自动处理了偏差问题。

API 设计建议

原来的写法 randomInt(num, offset) 语义模糊——不清楚 num 是最大值还是范围长度:

// ❌ 不直观
randomInt(10, 5)   // 5~14?还是 0~5 + 10 的偏移?

改成 (min, max) 更清晰:

// ✅ 直观
randomInt(5, 14)  // [5, 14] 范围内均匀取整

场景选择

场景推荐
UI 动画、随机颜色Math.random()
随机数组元素、占位数据Math.random()
验证码、tokencrypto.getRandomValues()
抽奖、游戏随机crypto.getRandomValues() + 拒绝采样
Node.js 服务端crypto.randomInt()

性能上 Math.random()crypto.getRandomValues() 快一个数量级,安全要求不高时不必引入加密随机数的开销。