☰
PHP8.3的随机数生成器怎么选择算法
2026/10/2 10:17:36 网站建设 项目流程

前言

升级到 PHP 8.x 之后,很多人第一次看到Random\Randomizer和四个Random\Engine\*类时都会愣一下:以前一个mt_rand()走天下,现在让我「选择算法」,到底该选哪个?

先说清楚版本关系,因为标题里的版号需要修正:Random\Randomizer这套面向对象的随机数 API 是 PHP 8.2 引入的,不是 8.3。PHP 8.2 带来了引擎(Engine)与随机化器(Randomizer)的分离,以及Mt19937、PcgOneseq128XslRr64、Xoshiro256StarStar、Secure四个内置引擎。PHP 8.3 是在此基础上做加法,补上了Random\Randomizer::getFloat()、Random\Randomizer::getBytesFromString()和边界枚举Random\IntervalBoundary。所以本文的标题虽然写的是 8.3,但一切以 8.2 为基础,涉及 8.3 新增的方法会单独标出。

真正要解决的问题只有一个:这个随机数如果被别人猜到,后果有多严重?后果严重(令牌、验证码、抽奖结果、加密用途)就必须用密码学安全的引擎;后果不严重(洗牌、分流、造测试数据)用非安全引擎换取速度和可复现性。本文给出选择标准、一张对照表和可以直接跑的验证脚本。

一、引擎与随机化器是两层

新的 API 把「产生随机字节」和「把字节变成有用的值」拆成了两层:

层职责类你需要关心的
引擎 Engine源源不断吐出随机字节实现Random\Engine接口安全性、速度、能否设种子
随机化器 Randomizer把字节转成整数/浮点/字符串/洗牌结果Random\Randomizer取值的边界、分布是否正确

四个内置引擎:

引擎可设种子密码学安全典型用途
Random\Engine\Mt19937可以否与老代码mt_rand()行为对齐、需要固定序列的模拟
Random\Engine\PcgOneseq128XslRr64可以否通用非安全场景:洗牌、抽样、造数据
Random\Engine\Xoshiro256StarStar可以否同上,另一套统计特性与状态大小
Random\Engine\Secure不可以是所有安全敏感场景,也是默认引擎

这里最关键的一行是Secure那一行:它不可设种子。因为它取的是操作系统提供的密码学安全随机源(Windows 上是BCryptGenRandom一类接口,类 Unix 上是getrandom(2)或/dev/urandom),没有「种子」这个概念。这直接决定了它的用途——能复现的随机数一定不安全,安全的随机数一定不能复现。

二、怎么选:先问「被猜到会怎样」

场景后果选择
密码重置令牌、邮箱验证码、API Key账号被接管Random\Engine\Secure(或random_bytes())
抽奖中奖者、优惠券发放顺序可被薅羊毛Random\Engine\Secure
加密盐、初始化向量加密体系被破Random\Engine\Secure
洗牌、随机排序、A/B 分流用户体验受影响PcgOneseq128XslRr64或Xoshiro256StarStar
造测试数据、压测流量无上述任一非安全引擎 + 显式种子
复现某个历史随机序列无Mt19937+ 与老代码相同的种子

三条实操规则:


  1. 不确定就用默认的。new Random\Randomizer()不传引擎时用的是Secure。性能上它不是瓶颈——现代系统的 CSPRNG(密码学安全伪随机数生成器)本来就是为通用场景设计的;真正的开销差异要看你每秒要取多少个值,而绝大多数 Web 请求每秒取的随机数用十个手指头数得过来。

  2. 需要复现就一定不是安全场景。只要你在构造函数里传了种子,就说明这个随机序列允许被别人知道,那就别拿它生成任何凭证。

  3. 非安全引擎三选一不用纠结。Mt19937的价值主要是「与mt_rand()一致」,用于兼容老代码;新写的非安全场景直接用PcgOneseq128XslRr64或Xoshiro256StarStar,哪个更快请自己在目标环境上实测,不同 PHP 版本与 CPU 架构下的排序并不总是一致。


另外要记住:换了引擎不等于换了Randomizer的方法。getInt()、shuffleArray()这些方法在哪个引擎下都可用,它们的区别只在于底层字节从哪来。

三、安全边界:这些函数不是密码学安全的

新 API 不会自动保护你——只要你还在用老函数,风险照旧。

函数密码学安全说明
random_bytes()/random_int()是PHP 7.0 起提供,底层就是 CSPRNG
Random\Engine\Secure是8.2 起
new Random\Randomizer()(不传引擎)是内部用Secure
rand()/mt_rand()否可预测,仅凭少量输出即可还原内部状态
shuffle()/str_shuffle()/array_rand()否使用非密码学的内部随机源
uniqid()否基于微秒时间戳,可枚举
lcg_value()否线性同余,统计质量差

也就是说,把mt_rand(100000, 999999)当短信验证码发出去,是这套 API 里最危险的一种用法——攻击者拿到几个验证码样本后,理论上可以推算出后续序列。

四、PHP 8.3 新增的方法

在 8.2 的基础上,8.3 补齐了几块明显的短板:

<?php declare(strict_types=1); // 需要 PHP 8.3+ $r = new Random\Randomizer(); // 默认使用 Secure 引擎 // 1) 取浮点数,并指定边界开闭:getFloat(float $min, float $max, IntervalBoundary $boundary) $lat = $r->getFloat(-90, 90, Random\IntervalBoundary::ClosedClosed); // [-90, 90] $lng = $r->getFloat(-180, 180, Random\IntervalBoundary::OpenClosed); // (-180, 180] // 2) 从指定字符集中取字节组成字符串:getBytesFromString(string $string, int $length) $token = $r->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 32);

IntervalBoundary枚举的四个取值,是把数学区间记法直接搬了过来:

枚举值区间含义
ClosedOpen[min, max)默认值,含下界不含上界
ClosedClosed[min, max]上下界都可取到
OpenClosed(min, max]不含下界含上界
OpenOpen(min, max)上下界都取不到

为什么浮点数要专门搞个边界参数?因为浮点数无法在任意区间里做到「每个值等概率」——需要决定端点是否允许被返回,以及如何处理舍入。getFloat()把这个决策显式地摆到参数上,而不是像老代码那样靠mt_rand() / mt_getrandmax()自己乘,是对的。

getBytesFromString()有个很实用的性质:字符在源字符串里重复出现,就等于给它加权。这在抽奖里非常方便:

<?php declare(strict_types=1); // 需要 PHP 8.3+ $r = new Random\Randomizer(); // 'a' 出现 3 次,'b' 出现 1 次 —— 抽到 a 的概率是 75% $winner = $r->getBytesFromString('aaab', 1); // 'a' 或 'b'

注意它是按字节挑选,不是按字符。源字符串里如果有中文(UTF-8 下一个汉字占 3 字节),长度的语义就变成了字节数,很容易把一个汉字切成半个。加权字符集请只用 ASCII。

五、完整可运行示例

把下面这段存成rand_demo.php直接运行(需要 PHP 8.3+)。它包含四个真实场景,以及一段让你在自己机器上实测的性能对比:

<?php declare(strict_types=1); // 需要 PHP 8.3+(getFloat / getBytesFromString / IntervalBoundary 均为 8.3 新增) // ---------- 1. 安全场景:密码重置令牌 ---------- function passwordResetToken(int $bytes = 32): string { // 默认引擎就是 Secure,这里显式写出来是为了让审阅者一眼看到安全意图 $r = new Random\Randomizer(new Random\Engine\Secure()); return bin2hex($r->getBytes($bytes)); } echo '重置令牌:' . passwordResetToken() . "\n"; // ---------- 2. 安全场景:6 位数字验证码(避免取模偏差)---------- function numericOtp(int $digits = 6): string { $r = new Random\Randomizer(); $out = ''; for ($i = 0; $i < $digits; $i++) { // getInt 的上下界都是闭区间 $out .= (string) $r->getInt(0, 9); } return $out; } echo '验证码:' . numericOtp() . "\n"; // ---------- 3. 非安全但需要复现:固定种子的洗牌 ---------- function seededShuffle(array $items, int $seed): array { $r = new Random\Randomizer(new Random\Engine\PcgOneseq128XslRr64($seed)); return $r->shuffleArray($items); } $deck = range(1, 10); $a = seededShuffle($deck, 20240929); $b = seededShuffle($deck, 20240929); printf("种子相同结果相同:%s\n", $a === $b ? '是' : '否'); printf("洗牌结果:%s\n", implode(',', $a)); // ---------- 4. 加权抽奖 + 浮点坐标 ---------- $r = new Random\Randomizer(); $prizes = $r->getBytesFromString('aaabbc', 1); // a:50% b:33.3% c:16.7% echo "抽奖结果:$prizes\n"; $lat = $r->getFloat(-90, 90, Random\IntervalBoundary::ClosedClosed); $lng = $r->getFloat(-180, 180, Random\IntervalBoundary::OpenClosed); printf("随机坐标:%.6f, %.6f\n", $lat, $lng); // ---------- 5. 性能对比:在你的机器上自己跑,我不给结论 ---------- function bench(string $label, callable $fn, int $rounds = 200000): void { $t0 = hrtime(true); for ($i = 0; $i < $rounds; $i++) { $fn(); } $ms = (hrtime(true) - $t0) / 1e6; printf("%-34s %d 次共用 %.1f ms\n", $label, $rounds, $ms); } $secure = new Random\Randomizer(new Random\Engine\Secure()); $pcg = new Random\Randomizer(new Random\Engine\PcgOneseq128XslRr64(1)); $xoshiro = new Random\Randomizer(new Random\Engine\Xoshiro256StarStar(1)); bench('Secure::getInt', static fn () => $secure->getInt(0, 1000)); bench('PcgOneseq128XslRr64::getInt', static fn () => $pcg->getInt(0, 1000)); bench('Xoshiro256StarStar::getInt', static fn () => $xoshiro->getInt(0, 1000)); bench('mt_rand(老函数)', static fn () => mt_rand(0, 1000)); bench('random_int(老函数)', static fn () => random_int(0, 1000));

在一台普通开发机上,输出大致长这样(具体数字每次都不一样,请以你自己跑出来的为准):

重置令牌:9f2c1b7e4a08d3... 验证码:471902 种子相同结果相同:是 洗牌结果:6,2,9,1,7,4,10,3,5,8 抽奖结果:a 随机坐标:-13.482910, 176.201385 Secure::getInt 200000 次共用 ... ms PcgOneseq128XslRr64::getInt 200000 次共用 ... ms Xoshiro256StarStar::getInt 200000 次共用 ... ms mt_rand(老函数) 200000 次共用 ... ms random_int(老函数) 200000 次共用 ... ms

跑完你就有了自己环境上的真实数据。如果Secure与PcgOneseq128XslRr64的差距在你的业务量级下可以忽略(对绝大多数 Web 应用来说是这样),那唯一正确的选择就是默认的Secure。

常见坑点

1. 用mt_rand()生成令牌

❌$token = md5(mt_rand());——Mt19937不是密码学安全的,观察到足够多的输出后可以还原内部状态并预测后续值;md5也改变不了这一点。 ✅bin2hex(random_bytes(32)),或(new Random\Randomizer())->getBytes(32)。

2. 用时间当种子

❌new Random\Engine\Mt19937(time());—— 种子空间只有几千万种可能,攻击者枚举一遍就能复现你的「随机」序列。 ✅ 需要复现的场景,种子来自配置或测试固件;需要安全的场景,用Secure,它压根不给你种子。

3. 拿Secure引擎想要「可复现」

❌new Random\Randomizer(new Random\Engine\Secure())然后指望两次运行结果一致 ——Secure不接受种子,输出永远不可复现。 ✅ 要复现就换成Mt19937/PcgOneseq128XslRr64/Xoshiro256StarStar并显式传种子,同时确认这个随机数不用于安全用途。

4. 忘了getInt()的上下界都是闭区间

❌ 想取一个 0 到 99 的值却写$r->getInt(0, 100),结果偶尔出现 100;或者想取[1, 6]的骰子写成getInt(1, 5)。 ✅getInt($min, $max)两端都可取到。要[0, 100)就写getInt(0, 99);$min大于$max会直接抛ValueError。

5. 用shuffle()/str_shuffle()生成安全相关的顺序

❌ 用str_shuffle('0123456789')取前 6 位做验证码 —— 这些函数用的是非密码学的内部随机源,且内部算法是公开的洗牌。 ✅ 用$r->shuffleArray($digits)($r为默认的安全引擎),或者直接numericOtp()那样逐位getInt()。

6. 用getBytesFromString()处理非 ASCII 字符集

❌$r->getBytesFromString('甲乙丙丁', 2)—— 它是按字节挑的,会从汉字中间切开,产出非法 UTF-8。 ✅ 加权字符集只用 ASCII;要处理多字节字符就先按字符数组取索引,再映射回字符。

7. 把引擎对象序列化进缓存或 Session

❌ 把Random\Engine存进 Session 或 Redis,指望跨请求延续序列 —— 引擎的内部状态是随机数质量的一部分,序列化与共享会带来不可预期的分布问题,也容易在并发下互相干扰。 ✅ 引擎只在单个请求/单次计算内创建;需要跨请求的「随机」就用数据库或缓存里的持久化值本身。

8. 假设随机 API 永远不会失败

❌$t = random_bytes(16);完全不考虑系统熵源不可用的情况 —— 它会抛异常,未捕获就是 500。 ✅ 用try { ... } catch (Throwable $e) { /* 记录并降级为不可发放凭证 */ }包住;异常的具体类型随版本变化,捕获Throwable最稳。真正要记住的是:熵源拿不到时必须拒绝发放凭证,而不是退回mt_rand()。

总结

问题答案
Random\Randomizer是哪个版本引入的PHP 8.2(标题里的 8.3 只对了后半段)
PHP 8.3 加了什么getFloat()、getBytesFromString()与边界枚举IntervalBoundary(getBytes()、getInt()、shuffleArray()在 8.2 就有)
默认引擎是什么Random\Engine\Secure
安全场景选哪个Secure,或者random_bytes()/random_int()
非安全但要快/要复现PcgOneseq128XslRr64/Xoshiro256StarStar/Mt19937(可设种子)
和mt_rand()对齐Random\Engine\Mt19937
老函数还能用吗能用,但rand/mt_rand/shuffle/array_rand/uniqid都不是密码学安全的


选算法这件事,说到底只有一句话:先判断「被猜到会怎样」,再决定用不用安全引擎。判断不了就一律用默认的Secure——它慢不到哪里去,而选错的代价是账号被接管。真正需要非安全引擎的场合只有两类:需要复现的测试与模拟,以及需要与老代码序列对齐的兼容场景。把这两类和业务凭证严格分开,选型就不会出错。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询