最稳妥方案是直接使用32字符白名单生成验证码:['A','C','D','E','F','G','H','J','K','M','N','P','Q','R','T','U','V','W','X','Y','3','4','6','7','9'],从源头排除易混淆字符,避免后续过滤、重试或绘图补救。

直接用白名单字符集最稳妥,别在生成后再过滤——后者容易陷入死循环或性能抖动。
用静态白名单数组替代原始字符池
易混淆字符(如 0、O、1、I、l、2、Z、5、S、8、B)不是“偶尔认错”,而是字体渲染下几乎无法区分。与其每次生成都检测剔除,不如从源头砍掉。
- 定义一个明确的 32 字符白名单:
['A', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', '3', '4', '6', '7', '9'] - 避免用
range('A', 'Z')再过滤——它隐含了I和O,容易漏 - 不要混用大小写:小写
l和大写I形态接近,统一用大写更安全 - 生成时直接用
array_rand($chars, $length)抽键,再implode('', array_map(fn($k) => $chars[$k], $keys))拼接
别用正则预校验法做主逻辑
preg_match('/[0OlI1]/', $code) 看似简单,但实际会放大失败率——尤其当验证码长度为 4 且字符池窄时,重试次数可能飙升。PHP 8.5+ 虽优化了 mt_rand() 性能,但循环重试仍是无谓开销。
- 该方法只适合兼容旧系统、不能改字符池的场景
- 若必须用,正则应写成
/^[^0OlI12Z5S8B]+$/(注意开头^和结尾$),否则会误判部分匹配 - 务必设最大重试次数(如 100 次),避免极端情况下卡住
- 不建议在高并发接口中使用此法,CPU 毛刺明显
GD 绘图阶段屏蔽是无效补救
有人想在 imagechar() 或 imagettftext() 前检查单个字符是否在混淆列表里,跳过绘制——这解决不了根本问题:验证码字符串本身已含歧义字符,用户看到的就是错的。
立即学习“PHP免费学习笔记(深入)”;
- 绘图时过滤只能防止显示,但 session 里存的仍是错误字符串,验证必然失败
- 若强行在绘图环节替换字符(如把
0改画成O),会导致前后端不一致 - 真正要做的,是在字符串生成完毕、写入
$_SESSION['captcha_code']之前就确保它干净
最常被忽略的一点:白名单字符集一旦定下,前端输入框也要同步限制可输入字符(用 pattern 或 JS 校验),否则用户输进 0 或 l,后端比对时即使做了 strtolower() 也救不回来——因为服务端压根没生成过这些字符。



















