PHP8.3的随机扩展怎么生成加密安全字符串

发布时间:2026/10/2 4:20:19
PHP8.3的随机扩展怎么生成加密安全字符串 前言先纠正一个版本问题Random\Randomizer这套随机扩展random extension是 PHP 8.2 引入的不是 8.3。标题里的版号有误本文按 8.2 讲。不过 PHP 8.3 确实给这套扩展补了一个很实用的方法Randomizer::getBytesFromString()所以用 8.3 生成随机字符串这个方向本身没错——只是新扩展的起点在 8.2。为什么值得专门用这套扩展因为 PHP 里生成随机字符串的祖传写法几乎都不安全$token md5(uniqid((string) mt_rand(), true)); $code substr(str_shuffle(ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789), 0, 8);这两行代码流传极广但底层是mt_rand()/str_shuffle()用的是可预测的伪随机数生成器PRNGMt19937。攻击者观察到足够多的历史输出后就能反推内部状态预测后续所有结果。用它生成的令牌、会话 ID、验证码等于把钥匙放在门垫下面。PHP 8.2 之前安全随机源只有random_bytes()和random_int()。它们本身够安全但用起来别扭——想从自定义字母表里取 32 个字符得自己写拒绝采样消除取模偏差写错一步就前功尽弃。随机扩展把这套逻辑做成了内建 API。一、先搞清楚哪些是真随机源1.1 安全与不安全的分界线函数 / 类随机源可用于安全场景random_bytes()操作系统 CSPRNG是random_int()操作系统 CSPRNG是Random\Randomizer默认引擎操作系统 CSPRNG是Random\Engine\Secure操作系统 CSPRNG是rand()/mt_rand()Mt19937否shuffle()/str_shuffle()Mt19937否array_rand()Mt19937否uniqid()微秒级时间戳否lcg_value()线性同余否分界线在于随机源是不是操作系统提供的密码学安全伪随机数生成器CSPRNG。random_bytes()、random_int()以及Random\Randomizer的默认引擎Random\Engine\Secure都会去读系统的熵源Linux 上是getrandom()系统调用或/dev/urandom而rand()、mt_rand()系列完全在用户态用固定算法递推数学上可预测。uniqid()尤其容易被误用。它的输出是当前微秒时间戳的十六进制看起来随机实际上完全由时间决定——攻击者只要知道你大概什么时候生成的令牌搜索空间就只剩几万个候选暴力枚举毫无压力。而且在高并发下同一微秒内产生的uniqid()会直接碰撞。1.2 随机扩展的结构PHP 8.2 的随机扩展把引擎和随机化的用法拆成了两层类型作用Random\Engine接口提供字节流是随机性的来源Random\Engine\SecureCSPRNG 引擎读操作系统熵源Random\Engine\Mt19937传统 Mt19937可指定种子确定性Random\Engine\PcgOneseq128XslRr64PCG 算法速度快确定性Random\Engine\Xoshiro256StarStarXoshiro 算法确定性Random\Randomizer消费引擎输出提供取整数、取字节、洗牌等高层方法关键点Randomizer的构造函数参数可以为null此时它默认使用Random\Engine\Secure。也就是说new Random\Randomizer()拿到的就是密码学安全的随机源不需要额外配置。Randomizer提供的方法里与生成字符串相关的是这几个方法说明起始版本getBytes(int $length)取 N 个随机字节PHP 8.2getInt(int $min, int $max)取区间内整数无模偏差PHP 8.2getFloat(...)取浮点数边界行为由IntervalBoundary枚举控制PHP 8.2shuffleArray(array $array)用安全随机源打乱数组PHP 8.2shuffleBytes(string $bytes)用安全随机源打乱字节串PHP 8.2pickArrayKeys(array $array, int $num)随机取 N 个不重复的键PHP 8.2getBytesFromString(string $alphabet, int $length)从自定义字母表取字符组成字符串PHP 8.3getBytesFromString()正是标题里那个8.3的落脚点它是 PHP 8.3 新增的也是用它替代手写取模逻辑的核心理由。1.3 为什么手写取模会有偏差这是随机扩展真正帮你解决掉的坑值得单独讲清楚。假设字母表是 62 个字符0-9A-Za-z你从随机源取一个字节0~255然后对 62 取模$alphabet 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; // 62 个 $out ; for ($i 0; $i 16; $i) { $out . $alphabet[ord(random_bytes(1)) % 62]; }random_bytes(1)本身是密码学安全的但取模操作破坏了均匀性256 不是 62 的整数倍256 4 * 62 8。落在这个余数区间里的字节会被映射到字母表的前 8 个字符上多出一次机会。字母表下标命中的字节值个数概率0 ~ 755/256 ≈ 1.953%8 ~ 6144/256 1.5625%前 8 个字符的出现概率比其余字符高出 25%。对 16 位的令牌来说这会显著削减实际搜索空间。如果字母表是 36 个字符256 7 * 36 4偏差会更大。二、用随机扩展生成各种令牌2.1 一个可直接运行的令牌生成器?php // token-generator.php —— 需要 PHP 8.3用到 Randomizer::getBytesFromString declare(strict_types1); use Random\Engine\Secure; use Random\Randomizer; final class TokenGenerator { private const BASE62 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; // 去掉容易读错抄错的 0 O 1 l I private const UNAMBIGUOUS 23456789ABCDEFGHJKLMNPQRSTUVWXYZ; private Randomizer $randomizer; // 传 null 时 Randomizer 默认使用 Random\Engine\Secure这里显式写出来 public function __construct(?Randomizer $randomizer null) { $this-randomizer $randomizer ?? new Randomizer(new Secure()); } /** 十六进制令牌32 字节 256 bit 熵 */ public function hexToken(int $bytes 32): string { if ($bytes 16) { throw new InvalidArgumentException(安全令牌至少需要 16 字节128 bit); } return bin2hex(random_bytes($bytes)); } /** 从自定义字母表取字符内部无模偏差 */ public function randomString(int $length, string $alphabet self::BASE62): string { if ($length 1 || strlen($alphabet) 2) { throw new InvalidArgumentException(长度必须大于 0字母表至少 2 个字符); } // getBytesFromString 按字节取字符字母表必须是单字节字符 if (strlen($alphabet) ! mb_strlen($alphabet, UTF-8)) { throw new InvalidArgumentException(字母表只能包含单字节字符); } return $this-randomizer-getBytesFromString($alphabet, $length); } public function shortCode(int $length 8): string { return $this-randomString($length, self::UNAMBIGUOUS); } /** 定长数字验证码取值含两端 */ public function numericCode(int $digits 6): string { if ($digits 4 || $digits 18) { throw new InvalidArgumentException(位数必须在 4 到 18 之间); } return (string) $this-randomizer-getInt(10 ** ($digits - 1), 10 ** $digits - 1); } public function shuffle(array $items): array { return $this-randomizer-shuffleArray($items); } public function sampleKeys(array $items, int $num): array { if ($num 1 || $num count($items)) { throw new InvalidArgumentException(抽取数量超出范围); } return $this-randomizer-pickArrayKeys($items, $num); } } $gen new TokenGenerator(); echo hexToken : , $gen-hexToken(32), PHP_EOL; echo randomString(24): , $gen-randomString(24), PHP_EOL; echo shortCode : , $gen-shortCode(8), PHP_EOL; echo numericCode : , $gen-numericCode(6), PHP_EOL; echo shuffle : , implode(,, $gen-shuffle([1, 2, 3, 4, 5])), PHP_EOL; $picked $gen-sampleKeys([a 1, b 2, c 3, d 4], 2); echo sampleKeys : , implode(,, $picked), PHP_EOL;每次运行的输出都不同字符集和长度固定。2.2 PHP 8.2 环境的替代实现如果生产环境暂时是 PHP 8.2没有getBytesFromString()可以自己写一个等价的方法。核心是拒绝采样rejection sampling把字节值域切成若干个完整区间落在尾部残缺区间里的字节直接丢弃重抽。?php // secure-alphabet-php82.php —— 需要 PHP 8.2 declare(strict_types1); function secureAlphabetString(string $alphabet, int $length): string { $size strlen($alphabet); if ($size 2 || $length 1) { throw new InvalidArgumentException(字母表至少 2 个字符长度必须大于 0); } // 只接受小于 limit 的字节limit 之前的值域能被 size 整除 // 因此不会出现某个字符概率偏高的情况 $limit intdiv(256, $size) * $size; $out ; while (strlen($out) $length) { $byte ord(random_bytes(1)); if ($byte $limit) { continue; // 落在残缺区间丢弃重抽 } $out . $alphabet[$byte % $size]; } return $out; } $token secureAlphabetString(0123456789abcdefghijklmnopqrstuvwxyz, 16); echo $token, (, strlen($token), ) , PHP_EOL;以 62 个字符的字母表为例limit 4 * 62 248字节值 248 到 255 这 8 个会被丢弃。每次抽取的接受概率是 248/256 ≈ 96.9%平均多抽不到 4% 的次数代价可以忽略。关键是绝不能用多取一个字节凑数之类的土办法去补偿偏差。字符集较小时可以一次多取几个字节减少random_bytes()的调用次数但对长度几十的令牌来说逐字节抽取的差异可以忽略可读性更重要。2.3 熵源不可用时的处理random_bytes()在无法获取足够熵时抛Random\RandomExceptionPHP 8.2 起8.1 及更早抛的是Exception。这个异常意味着系统出了严重问题绝对不能捕获之后降级到mt_rand()?php // entropy-failure.php —— 需要 PHP 8.2 declare(strict_types1); function issueResetToken(): string { try { return bin2hex(random_bytes(32)); } catch (Random\RandomException $e) { // 熵源不可用是基础设施故障宁可让请求失败 // 也不能退回到可预测的随机数生成器 throw new RuntimeException(系统随机源不可用拒绝签发令牌, 0, $e); } } // 调用方按普通异常处理最终返回 503而不是悄悄降级 echo substr(issueResetToken(), 0, 16), ..., PHP_EOL;三、生成之后还要做对的事随机字符串生成对了只是第一步存储和比较环节同样有坑。3.1 令牌不要明文入库数据库一旦泄露备份文件、日志、SQL 注入明文令牌等于直接把所有账号的登录凭证交出去。标准做法是入库前先哈希验证时比对哈希?php // token-storage.php —— 需要 PHP 7.4 declare(strict_types1); /** 生成令牌库里只留 SHA-256 摘要 */ function createToken(): array { $token bin2hex(random_bytes(32)); $digest hash(sha256, $token); // 令牌熵足够高无需加盐或慢哈希 return [token $token, digest $digest]; // token 只发给用户digest 落库 } function verifyToken(string $presented, string $storedDigest): bool { $computed hash(sha256, $presented); // 恒定时间比较避免通过响应时间差逐步猜出正确摘要 return hash_equals($storedDigest, $computed); } $issued createToken(); var_dump(verifyToken($issued[token], $issued[digest])); // true var_dump(verifyToken($issued[token] . ff, $issued[digest])); // false var_dump(verifyToken(, $issued[digest])); // false这里不用password_hash()是因为令牌本身就是 256 bit 的高熵随机值没有猜得到的弱口令需要靠慢哈希去抵抗暴力枚举用一次 SHA-256 即可还能省掉每次验证几十毫秒的开销。密码则必须用password_hash()两者场景不同不要互相替代。3.2 令牌长度怎么定长度直接对应暴力搜索的空间大小按 2 的幂次算用途建议长度熵说明会话 ID16 字节128 bit会话生命周期短128 bit 足够API 令牌 / 重置令牌32 字节256 bit长期有效按 256 bit 配邮箱验证链接32 字节256 bit同上短信验证码6 位十进制约 20 bit靠限速和有效期保护不靠长度人工抄写短码8 位大写字母数字约 40 bit必须配合短有效期和一次性使用规律是能被脚本反复尝试的场景靠长度必须靠人输入的场景靠限速。短信验证码只有 6 位理论上百万次就能撞中它靠的是每个手机号一分钟只能发一次、错误五次锁定这类策略。而重置令牌会被程序自动提交没有交互成本所以必须用 256 bit。3.3 测试里要用确定性引擎生产必须用Secure但单元测试里随机的断言写起来很痛苦。这时用带固定种子的Mt19937引擎让随机序列可复现?php // deterministic-test.php —— 需要 PHP 8.2 declare(strict_types1); use Random\Engine\Mt19937; use Random\Randomizer; // 固定种子 每次运行产生完全相同的序列 $make static fn(int $seed 42): Randomizer new Randomizer(new Mt19937($seed)); $a $make(42)-getBytesFromString(abcdef, 8); $b $make(42)-getBytesFromString(abcdef, 8); var_dump($a $b); // true同一个种子必然复现 echo $a, PHP_EOL, $b, PHP_EOL;Mt19937是确定性算法同一个种子一定产出同一个序列因此断言稳定、失败可复现。但它只能出现在测试或统计模拟里绝不能用来生成任何真实凭证——代码审查时看到new Mt19937()出现在业务路径上就应该当作安全缺陷处理。常见坑点❌ 用md5(uniqid((string) mt_rand(), true))生成密码重置令牌 ✅ 用bin2hex(random_bytes(32))或者new Random\Randomizer()配合它的各个方法❌ 用str_shuffle()或shuffle()打乱后截取来生成验证码 ✅ 这两个函数基于 Mt19937输出可预测改用Randomizer::shuffleArray()或getBytesFromString()❌ 自己写ord(random_bytes(1)) % strlen($alphabet)从字母表取字符 ✅ 取模会引入偏差62 字符字母表下前 8 个字符概率高 25%PHP 8.3 用Randomizer::getBytesFromString()PHP 8.2 用拒绝采样❌ 给getBytesFromString()传含中文的字母表 ✅ 它按字节取字符多字节字符会被切成非法 UTF-8 片段字母表必须是纯 ASCII❌ 把生成的令牌明文存进数据库 ✅ 存hash(sha256, $token)的摘要验证时用hash_equals()做恒定时间比较❌ 用Random\Engine\Mt19937或者Random\Engine\Xoshiro256StarStar生成业务令牌 ✅ 它们是确定性引擎泄露种子即可完整预测输出只有Secure才是 CSPRNG测试里才用确定性引擎❌ 捕获Random\RandomException之后降级到mt_rand()✅ 熵源不可用是基础设施级故障应当直接返回 503 让请求失败而不是悄悄降低安全性❌ 用random_int()拼出数字串再截断来当固定长度的验证码 ✅ 截断会破坏均匀分布直接用Randomizer::getInt($min, $max)取区间内的整数总结需求推荐写法最低版本十六进制令牌bin2hex(random_bytes(32))PHP 7.0自定义字母表字符串Randomizer::getBytesFromString($alphabet, $len)PHP 8.38.2 环境的等价实现拒绝采样去掉残缺区间PHP 8.2定长数字串(new Randomizer())-getInt($min, $max)PHP 8.2安全洗牌Randomizer::shuffleArray()PHP 8.2随机抽取 N 个不重复项Randomizer::pickArrayKeys($array, $n)PHP 8.2存储令牌只存hash(sha256, $token)PHP 7.0比较令牌hash_equals()PHP 5.6单元测试new Randomizer(new Mt19937($seed))PHP 8.2禁止使用rand、mt_rand、shuffle、str_shuffle、array_rand、uniqid—记住三件事就够了随机扩展是 PHP 8.2 引入的getBytesFromString()是 8.3 补上的只有Secure引擎和random_bytes()是密码学安全的以及生成的令牌落库前一定要先哈希。把这三条守住构造随机字符串这件事就不会成为整个系统的短板。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询