mt_rand() 和 rand() 不适合生成 Token,因其基于线性同余算法、输出可预测,PHP 7.1+ 已明确不适用于密码学场景;攻击者可通过少量已知 Token 反推种子并批量生成有效会话 ID。

为什么 mt_rand() 和 rand() 不适合生成 Token
它们基于线性同余算法,输出可预测,且在 PHP 7.1+ 中已明确不适用于密码学场景。攻击者通过少量已知 Token 就能反推种子,批量生成有效会话 ID。
真正安全的 Token 必须满足:不可预测、高熵、无状态、抗时序攻击。这意味着必须用操作系统级的加密随机源。
-
/dev/urandom(Linux/macOS)或CryptGenRandom(Windows)是唯一可信来源 -
openssl_random_bytes()是 PHP 对上述源的封装,PHP 5.6+ 默认启用,无需额外扩展 -
random_bytes()(PHP 7.0+)是更现代的替代,语义更清晰,优先选它
用 random_bytes() 生成定长 Token 的正确写法
直接输出二进制字节是危险的——无法安全传输、易被截断、日志中可能乱码。必须编码,但 Base64 编码含 +、/、=,不适合 URL 或 Cookie 场景;hex 编码又太长(2 倍膨胀)。
推荐使用 bin2hex() + 截断,或 base64_encode() 后清理非法字符:
立即学习“PHP免费学习笔记(深入)”;
function generateToken(int $length = 32): string
{
// $length 指最终字符串长度,不是字节数
$bytes = random_bytes(ceil($length / 2));
return substr(bin2hex($bytes), 0, $length);
}
说明:
- 传入
$length = 32→ 实际取 16 字节 → hex 后得 32 字符,熵值 ≈ 128 bit - 不用
base64_encode()是因需额外替换+//,且长度不整除 4 时补=,增加解析负担 - 避免用
uniqid()+mt_rand()拼接——时间戳部分可被枚举,整体熵严重不足
Token 存储和校验时的两个隐形陷阱
生成再强,存错或比对错,等于白做。
- 存储时别用
md5($token)或sha1($token)—— 这会让攻击者发起碰撞或彩虹表预计算;应改用password_hash($token, PASSWORD_ARGON2ID)或至少hash_hmac('sha256', $token, $secret_key) - 校验时禁用
==或strcmp()—— 它们存在时序侧信道;必须用hash_equals($expected, $given),它强制恒定时间比较 - Token 一旦使用(如登录成功),立即失效并生成新 Token,防止重放;不要依赖过期时间“代替”一次性使用
为什么不用 UUID v4?
UUID v4 虽然看起来随机,但其规范只要求 6 位固定、122 位伪随机,实际实现常依赖 mt_rand()(如 ext/uuid 或早期 com_create_guid()),PHP 8.2+ 的 Uuid::uuid4() 才默认用 random_bytes()。
更关键的是:UUID 是设计用来唯一,不是用来保密。它的结构化格式(如 xxxx-xxxx-xxxx-xxxx)暴露了生成逻辑,且长度固定、字符集受限,熵密度低于同等长度的纯 hex Token。
结论:除非你明确需要全局唯一标识且接受非密码学安全,否则别把 UUID v4 当会话 Token 用。
真正难的不是生成,是确保整个生命周期里 Token 不被泄露、不被重放、不被推测——每个环节都得绷着那根弦。



















