随机数生成器到底是怎么工作的

多数随机数代码都带着细微的偏置。下面说偏在哪,以及为什么它平时无所谓、直到不再无所谓。

随机数 发布于 2026年10月20日 阅读约 2 分钟

伪随机与密码学安全

PRNG 是带内部状态的确定性算法。给定相同的种子,它产生相同的序列——这对模拟是特性,对密码是致命缺陷。Math.random() 就是其中之一:每个页面播种一次,只要观察到足够多的输出就能预测。

CSPRNG 从操作系统的熵池取数,其设计目标是:即使你看到了它过去的输出,也无法据此预测它未来的输出。浏览器通过 crypto.getRandomValues() 把它暴露给页面。

modulo bias 问题

常见的捷径是 `randomUint32() % n`。看起来没问题,但 2^32 对多数 n 值都除不尽。如果 n 是 3,那么 0、1、2 各自的机会分别是 1,431,655,765、1,431,655,765 和 1,431,655,766——对 2 的微小偏置,无论取多少样本都不会消失。

解决办法是 rejection sampling:先算出 2^32 之内 n 的最大倍数,把任何超过这个上限的取值丢弃,然后才对留下来的数取余。代价是偶尔需要重抽一次,而偏置被彻底消除。

浮点数让问题更糟

`Math.floor(Math.random() * n)` 在 n 很大时有同一类问题,另外双精度浮点数只有 53 位尾数——超过 2^53 有些整数根本无法表示,所以更宽的区间会静默丢值。

处理小数时,可靠的做法是放大成整数、取数、再除回去。全程用浮点运算会引入舍入误差,可能把结果推到请求区间之外。

为什么均匀性对抽奖重要

0.00000002% 的偏置对掷骰子无关紧要,对抽奖同样无关紧要。但同一个有偏置的函数如果用来生成会话令牌或加密密钥,就是另一回事:偏置恰恰是攻击者可以用来压缩搜索空间的那种结构。

实用规则是这样的:纯装饰性的效果用 Math.random() 就够了;任何有人能靠预测获利的场合,都要用带 rejection sampling 的 CSPRNG。

常见问题

Math.random() 用于随机抽取够用吗?

选个餐馆,够用。抽奖发奖,请用密码学随机源,这样结果无法被预测,也无法被任何盯着页面的人复现。

用一句话解释 modulo bias?

当随机整数区间无法整除生成器的输出区间时,某些结果会变得比其他结果略微更容易出现。

浏览器能产生真随机吗?

不能。真随机需要物理熵源。操作系统从硬件事件中收集熵并为 CSPRNG 播种,这是安全上的实用标准。

配合这篇指南使用的工具

更多指南