隨機密碼產生是帳號註冊、重設密碼、API Key 發放等場景的基礎需求。早期常見做法使用 mt_rand() 從字元池抽樣,但 Mersenne Twister 非加密安全,輸出可被預測。本文從經典實作出發,逐步引入現代 random_bytes() / random_int() (CSPRNG) 最佳實踐,並提供可直接用於生產環境的完整函式庫。
function genPwd(int $length = 6): string
{
$pool = '23456789bcdfghjkmnpqrstvwxyz'; // 排除易混淆字元
$pwd = '';
for ($i = 0; $i < $length; $i++) {
$pwd .= $pool[mt_rand(0, strlen($pool) - 1)];
}
return $pwd;
}
問題:mt_rand() 種子空間只有 32 位元,觀察 624 次輸出即可破解後續序列;strlen() 在循環中重複呼叫;模數偏差 (modulo bias) 導致字元機率不均。
<?php
/**
* 產生密碼學安全的隨機密碼
*
* @param int $length 密碼長度 (建議 12-16+)
* @param string $mode 'alphanum' | 'alphanum_special' | 'pin' | 'hex' | 'base64'
* @param bool $excludeAmbiguous 是否排除易混淆字元 (l, 1, O, 0 等)
* @return string
* @throws Exception 當 random_bytes 失敗
*/
function generateSecurePassword(
int $length = 16,
string $mode = 'alphanum_special',
bool $excludeAmbiguous = true
): string {
// 定義字元池
$pools = [
'alpha' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
'numeric' => '0123456789',
'special' => '!@#$%^&*()-_=+[]{}|;:,.<>?',
'ambiguous' => 'l1IoO0', // 易混淆字元
];
// 組合目標字元池
$pool = match ($mode) {
'alphanum' => $pools['alpha'] . $pools['numeric'],
'alphanum_special' => $pools['alpha'] . $pools['numeric'] . $pools['special'],
'pin' => $pools['numeric'],
'hex' => '0123456789abcdef',
'base64' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
default => throw new InvalidArgumentException("Unknown mode: $mode"),
};
if ($excludeAmbiguous && $mode !== 'hex' && $mode !== 'base64') {
$pool = str_replace(str_split($pools['ambiguous']), '', $pool);
}
$poolLen = strlen($pool);
if ($poolLen === 0) throw new RuntimeException('Character pool is empty');
// 使用 random_int 避免模數偏差 (Rejection Sampling)
$bytesNeeded = (int)ceil($length * log($poolLen, 256)) + 3; // 多取緩衝
$randomBytes = random_bytes($bytesNeeded);
$result = '';
$idx = 0;
while (strlen($result) < $length) {
if ($idx >= strlen($randomBytes)) {
$randomBytes .= random_bytes($bytesNeeded);
$idx = 0;
}
$byte = ord($randomBytes[$idx++]);
// 拒絕採樣:只接受落在 [0, floor(256/poolLen)*poolLen) 範圍的位元組
$maxValid = floor(256 / $poolLen) * $poolLen;
if ($byte < $maxValid) {
$result .= $pool[$byte % $poolLen];
}
}
return $result;
}
/** 便利函式:產生 API Key (Base64URL 安全) */
function generateApiKey(int $entropyBytes = 32): string
{
return rtrim(strtr(base64_encode(random_bytes($entropyBytes)), '+/', '-_'), '=');
}
/** 便利函式:產生數字驗證碼 (PIN) */
function generatePin(int $digits = 6): string
{
return generateSecurePassword($digits, 'pin', false);
}
random_bytes() / random_int()?/dev/urandom (getrandom)、Windows BCryptGenRandom、FreeBSD /dev/urandom。直接 $byte % $poolLen 會使前 256 % $poolLen 個字元機率略高。拒絕採樣只接受能整除字元池大小的位元組範圍,確保均勻分佈。
| 用途 | 建議長度 | 字元池大小 | 熵 |
|---|---|---|---|
| 一般密碼 | 16 | ~72 (字母+數字+符號) | ~99 bits |
| 高安全密碼 | 24 | ~72 | ~149 bits |
| API Key | 32 bytes | 64 (Base64URL) | 256 bits |
| 簡訊驗證碼 | 6 | 10 | ~20 bits (需配合速率限制) |
<?php
require_once 'password_generator.php';
// 使用者密碼
$userPwd = generateSecurePassword(16); // alphanum_special, 排除易混淆
echo "User Password: $userPwd\n";
// 管理員高強度密碼
$adminPwd = generateSecurePassword(24);
echo "Admin Password: $adminPwd\n";
// API Key (給第三方整合)
$apiKey = generateApiKey(32);
echo "API Key: $apiKey\n";
// 簡訊驗證碼
$smsCode = generatePin(6);
echo "SMS Code: $smsCode\n";
// 十六進位 Token (如 CSRF Token)
$csrf = generateSecurePassword(32, 'hex');
echo "CSRF Token: $csrf\n";
composer require bjeavons/zxcvbn-php
<?php
use ZxcvbnPhp\Zxcvbn;
$zxcvbn = new Zxcvbn();
$password = generateSecurePassword(16);
$strength = $zxcvbn->passwordStrength($password);
echo "分數: {$strength['score']}/4 (0=極弱, 4=極強)\n";
echo "預估破解時間: {$strength['crack_times_display']['offline_slow_hashing_1e4_per_second']}\n";
echo "建議: " . implode('; ', $strength['feedback']['suggestions']) . "\n";
// 若分數 < 3 可重新產生
uniqid()、md5(time())、rand() 都不安全:輸出可預測、熵極低。shuffle(str_split($pool)) 仍依賴 mt_rand:不可用於密碼產生。password_hash():產生器只負責產生,儲存是另一層防護。現代 PHP 隨機密碼產生的鐵律:唯一正確來源是 random_bytes() / random_int()。配合拒絕採樣確保均勻分佈、可配置字元池滿足不同場景、整合 zxcvbn 驗證強度。將上述函式封裝為專案共用庫,統一管理密碼策略,避免各處複製貼上不一致的實作。