一、为什么需要crypto.randomInt()
Math.random()返回的浮点数来自一个确定性伪随机数生成器,其内部状态在理论上是可预测的。用它乘以区间长度再取整,不仅存在可预测性风险,还会因为浮点精度和取模操作引入分布偏差。对于会话标识、API密钥、重置令牌、抽奖序号这类对不可预测性有要求的场景,Math.random()不是合适的选择。
Node.js的crypto模块提供了操作系统级的密码学安全伪随机数来源。crypto.randomInt()正是在这一基础上封装的高层接口,它解决了两个问题:第一,直接返回整数,不需要手动从字节缓冲区换算;第二,通过拒绝采样避免了取模偏差(modulobias),使区间内每个整数被选中的概率相同。
二、语法与参数
crypto.randomInt([min,] max[, callback])
三个参数的含义如下:
min(可选,整数):随机区间的下界,包含在结果中。省略时默认值为0。
max(整数):随机区间的上界,不包含在结果中。返回的整数n满足min<=n<max。
callback(可选,函数):形如function(err,n){}。提供此参数时以异步方式调用;不提供时以同步方式返回结果。
关键约束:max-min必须小于2^48;min和max都必须是安全整数(safeinteger)。若参数不满足这些条件,会抛出ERR_OUT_OF_RANGE或ERR_INVALID_ARG_TYPE。
同步调用示例:
const { randomInt } = require('node:crypto');
const dice = randomInt(1, 7);
console.log(dice); // 1 到 6 之间的整数
异步调用示例:
const { randomInt } = require('node:crypto');
randomInt(0, 100, (err, n) => {
if (err) throw err;
console.log(n); // 0 到 99 之间的整数
});
三、版本差异与兼容性
crypto.randomInt()并非Node.js初始就有的接口。它于Node.jsv14.10.0和v12.19.0中引入。在此之前,开发者通常需要手动组合crypto.randomBytes()与拒绝采样逻辑来实现同等功能。
如果项目需要运行在Node.jsv12.19以下或v14.10以下的环境,有以下替代路径:
使用crypto.randomBytes()生成足够字节,读取为无符号整数,再通过拒绝采样映射到目标区间。需要自行处理取模偏差:计算range=max-min,取RAND_MAX-(RAND_MAX%range)作为接受上限,超出则重新采样。这种写法比直接%range更安全,但代码量明显增加。
对于新项目,建议将Node.js版本下限设为v14.10.0或更高的LTS版本,直接使用randomInt()。如果确实需要兼容旧版本,可以抽一个内部工具函数,在randomInt可用时调用原生方法,否则走randomBytes回退路径。
四、边界测试与常见错误
以下是一组针对参数边界的测试用例,可用于验证运行环境中的行为是否与预期一致:
const { randomInt } = require('node:crypto');
const assert = require('node:assert');
// 单参数模式:省略 min,等价于 randomInt(0, max)
for (let i = 0; i < 100; i++) {
const n = randomInt(10);
assert.ok(n >= 0 && n < 10);
}
// min 等于 max - 1:区间只有一个可能值
assert.strictEqual(randomInt(5, 6), 5);
// min > max:应抛出异常
assert.throws(() => randomInt(10, 5), /ERR_OUT_OF_RANGE/);
// 非整数参数:应抛出异常
assert.throws(() => randomInt(1.5, 10), /ERR_OUT_OF_RANGE/);
// 区间超出 2^48:应抛出异常
assert.throws(() => randomInt(0, 2 ** 48), /ERR_OUT_OF_RANGE/);
常见异常码说明:
| 异常码 | 触发条件 |
|---|---|
ERR_OUT_OF_RANGE |
min>=max,或区间超出2^48,或参数非安全整数 |
ERR_INVALID_ARG_TYPE |
参数类型不是number,或callback不是函数 |
在项目中,如果不确定调用方传入的区间是否合法,应在调用前做校验,而不是依赖异常来驱动流程。min<max和Number.isSafeInteger是两个必要的检查点。
五、与相邻函数的职责对比
crypto.randomInt()与几个容易混淆的函数在职责上有明确分界:
vsMath.random():后者不是密码学安全的,内部状态可被推断,不应用于任何安全敏感场景。crypto.randomInt()的随机源来自操作系统熵池,输出不可预测。
vscrypto.randomBytes():randomBytes返回Buffer,适合生成令牌、密钥、盐值等原始字节序列。randomInt返回整数,适合需要数值范围的场景(抽奖序号、验证码数字、游戏随机数)。两者都使用CSPRNG,但randomInt额外处理了区间映射和无偏采样。
vscrypto.randomUUID():randomUUID生成RFC4122v4UUID字符串,适合需要全局唯一标识符的场景。randomInt不保证唯一性,只保证在区间内均匀分布。如果需要一个短数字ID且不要求唯一,用randomInt;如果需要可全局标识的ID,用randomUUID。
vscrypto.getRandomValues():这是WebCryptoAPI的方法,在浏览器和Node.js的globalThis.crypto中可用。它填充TypedArray,不直接返回整数,也不做区间映射。如果在同构代码(浏览器+Node.js)中工作,可以用getRandomValues配合自定义拒绝采样;在纯Node.js环境中,randomInt的接口更直接。
六、性能考量与选用建议
crypto.randomInt()每次调用都会从操作系统熵源读取字节并执行拒绝采样循环。对于绝大多数业务场景(登录令牌、重置链接、单次抽奖),这个开销可以忽略。但如果在高吞吐路径中批量生成大量随机整数,同步调用会阻塞事件循环。
个人经验:在一个活动页的抽奖接口中,曾尝试在请求处理器中循环调用randomInt生成100个中奖序号,QPS较高时出现了可感知的事件循环延迟。后来改为用一次randomBytes(600)获取足够字节,在内存中按拒绝采样批量映射为整数,性能明显改善。这不是说randomInt有问题,而是说同步CSPRNG调用的次数需要根据吞吐量来权衡。
选用建议:
需要单个或少量安全随机整数时,直接用crypto.randomInt(),代码简洁且无偏。
需要批量生成时,考虑用randomBytes一次取足字节,自行实现拒绝采样。注意不要用简单的byte%range,那会引入偏差。
不需要不可预测性的场景(动画帧随机抖动、非安全相关的展示性随机排序),用Math.random()即可,不必调用CSPRNG。
异步版本randomInt(min,max,callback)在原理上与同步版本共享同一套底层逻辑,并不会因为“异步”而获得额外的安全收益。选择同步还是异步,取决于调用上下文是否允许阻塞:如果在启动阶段或脚本中生成少量值,同步版本足够;如果在高频请求路径中,异步版本可以减少对事件循环的即时占用,但本质上仍然是单次CSPRNG读取。
七、常见误区速查
误区一:randomInt(0,10)会返回10。不会。上界是排他的,返回范围是0到9。要包含10,应写randomInt(0,11)。
误区二:randomInt能保证多次调用结果不重复。不能。它只保证单次采样的分布均匀性,不维护历史记录。如果需要去重,应用层自行维护已使用。
误区三:异步版本的callback会在线程池中执行。callback本身在事件循环中运行,随机数的生成过程也不一定在独立线程中完成。异步形式的意义在于接口约定,而不是并行计算。
误区四:randomInt返回的是浮点数。返回的是整数(number类型中的安全整数),没有小数部分。
八、练习与思考
练习一:编写一个secureDice(rolls)函数,用crypto.randomInt模拟掷骰子rolls次,返回一个长度为rolls的数组,每个元素在1到6之间。要求对rolls做类型和范围校验。
练习二:实现一个不依赖crypto.randomInt的myRandomInt(min,max)函数,仅使用crypto.randomBytes(6)。提示:读取6字节为无符号整数,计算range,使用拒绝采样处理取模偏差。用统计测试对比你实现的函数与原生randomInt在[0,9]区间各100000次采样的频次分布。
思考题:在什么情况下,用crypto.randomBytes(4).readUInt32BE(0)%n是可以接受的?如果n是2的幂次(例如256),取模偏差还存在吗?请从数学角度解释。
延伸阅读与参考文献
Node.js官方文档——crypto.randomInt([min,]max[,callback])
https://nodejs.org/api/crypto.html#crypto_crypto_randomint_min_max_callback
最权威的语法、参数约束和异常说明来源。建议直接阅读当前LTS版本的对应章节。
Node.jsv14.10.0发布公告
https://nodejs.org/en/blog/release/v14.10.0
randomInt首次引入的版本记录,可用于确认较低版本要求。
Node.jsv12.19.0LTS发布公告
https://nodejs.org/en/blog/release/v12.19.0/
该版本将randomInt回溯到v12LTS线,是旧版本兼容判断的依据。
Safeguard博客——SecureRandomNumberGenerationinJavaScriptwithcrypto.randomBytes
https://safeguard.sh/resources/blog/secure-random-number-generation-in-javascript-with-cryptorandombytes
解释了CSPRNG与Math.random()的本质区别,以及randomBytes的操作系统熵源背景,有助于理解randomInt的安全根基。
StackOverflow——crypto.randomIntimplementationdiscussion
https://stackoverflow.com/revisions/4015444c-005a-46c7-9540-294593116fb0/view-source
展示了randomInt内部拒绝采样的简化实现逻辑,适合想理解“无模偏”具体做法的读者。
Bun运行时文档——crypto.randomInt
https://bun.sh/reference/node/crypto/randomInt
如果项目需要在Bun中运行,可参考此页确认接口兼容性与参数约束。