一、伪随机不是「假随机」
计算机里的随机数通常是伪随机:由一个确定的算法(如 Mersenne Twister、PCG)根据「种子」生成序列。特点:
- 看起来随机(统计上通过随机性检验)。
- 给定种子,序列完全可复现。
- 速度快,适合模拟、游戏、抽样。
「可复现」在调试和实验里是优点(同样的种子能重现问题),但在安全场景是致命缺点。
二、种子
import random
random.seed(42)
print(random.random()) # 每次都一样
不设种子时,多数语言用当前时间或系统熵初始化,所以每次运行不同。但只要种子可猜,序列就可预测。
有些老系统的漏洞就来自「用时间戳做种子」:攻击者知道大致时间,就能枚举出种子,进而预测出「随机」的 token。
三、安全场景要用密码学随机
// 不安全:不要用于 token、验证码、密码
Math.random()
// 安全
crypto.randomUUID()
crypto.getRandomValues(new Uint8Array(32))
import secrets
secrets.token_hex(16)
secrets.choice('abcdefghijklmnopqrstuvwxyz')
判断标准:这个随机数会不会影响安全(身份、金额、权限、抽奖结果)?会,就用 secrets / crypto 这类密码学安全的接口。
Math.random() 的实现(V8 用 xorshift128+)状态可以被反推,观察到足够多的输出就能预测后续值。
四、均匀分布与取模偏差
想生成 [0, 9] 的随机整数,直接 random() * 10 取整会有极小的不均匀;用「随机数 % n」在大范围内也有模偏差(当范围不是 n 的整数倍时,部分值概率略高)。
正确做法是用库提供的 randint(a, b) 或 secrets.randbelow(n),它们内部做了拒绝采样,保证均匀。
抽奖场景对公平性敏感,模偏差虽然极小,但应该用标准接口避免争议。
五、验证码与 token
- 验证码:位数少(4–6 位),必须配限流与过期,否则再随机也能被暴力枚举。
- 重置密码 token:至少 128 位熵,用密码学随机,一次性使用,短有效期。
- 邀请码:如果要「不可猜」,同样用安全随机;如果只是给人看的短码,必须配频率限制。
随机性只是第一层,限流、过期、一次性才是防爆破的关键。
六、可复现随机
需要「同样输入得到同样结果」时(如分桶实验、生成固定测试数据、游戏地图),用固定种子:
rng = random.Random(20261103)
group = rng.choice(['A', 'B'])
或者用哈希做确定性分桶:
bucket = hash(user_id + salt) % 100
这比随机分配更好:同一个用户每次进来都落在同一组,不会因为刷新而切组。
七、几个误区
- 「随机 = 不重复」:随机可能有重复。要不重复得用洗牌或集合去重。
- 「加个时间戳就随机了」:时间戳可预测,反而更危险。
- 「UUID 一定唯一」:v4 碰撞概率极低,但用弱随机源生成的「伪 UUID」不算。
- 「抽奖用 Math.random 没人能利用」:前端代码可见,攻击者能直接在控制台改结果,抽奖逻辑必须在服务端。
八、工具
用 随机生成器 生成随机数、随机字符串、抽签;生成强密码用 密码生成器;生成唯一标识用 UUID 生成器。这些工具都在浏览器本地运行,生成结果不会上传。
小结
伪随机由种子决定、可复现;安全场景用 secrets / crypto,不用 Math.random;取模有偏差,用标准 randint;验证码靠限流与过期而不是随机性;分桶用哈希而不是每次随机。