在PHP项目中,验证码是拦截恶意脚本的基础手段。很多框架自带了简单的数字验证码,但实际业务中往往需要根据安全等级调整字符范围、图像干扰和过期时间。理解GD库绘图与session存储的关系,才能稳妥地修改生成规则而不破坏校验逻辑。

一、验证码的基本工作原理
PHP验证码通常依赖GD库在服务器端动态绘制图片,并把正确字符串写入session。用户提交表单时,服务端比对session中的值与用户输入是否一致。因此修改生成规则,既要改图像绘制部分,也要保证写入session的内容与页面展示一致。
常见的默认实现只使用0到9的数字,字体单一、无干扰线,这样的图像很容易被OCR类工具批量识别。我们通过扩展字符集、增加曲线和噪点,可以提升机器识别成本。下面先看看最基础的数字验证码代码。
<?php
session_start();
$code = rand(1000, 9999);
$_SESSION['code'] = $code;
$img = imagecreatetruecolor(100, 40);
$bg = imagecolorallocate($img, 255, 255, 255);
imagefill($img, 0, 0, $bg);
$textColor = imagecolorallocate($img, 0, 0, 0);
imagestring($img, 5, 20, 10, $code, $textColor);
header('Content-type: image/png');
imagepng($img);
imagedestroy($img);
?>
二、修改字符集与生成规则
要改变验证码规则,第一步是定义自己的字符池。比如只保留不易混淆的大写字母与数字,避免0和O、1和I同时出现。然后利用随机函数从中抽取指定长度的组合,并存入session。这样前端展示的字符范围就完全受我们控制。
以下示例将验证码改为四位字母数字混合,并剔除易混淆字符。注意imagecreatetruecolor创建画布后,需要使用imagettftext加载字体文件,这样才能自由控制字体与角度,比imagestring更灵活。
<?php
session_start();
// 自定义字符池,去掉易混淆字符
$pool = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$length = 4;
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= $pool[rand(0, strlen($pool) - 1)];
}
$_SESSION['code'] = $code;
$width = 120;
$height = 50;
$img = imagecreatetruecolor($width, $height);
$bg = imagecolorallocate($img, 240, 240, 240);
imagefill($img, 0, 0, $bg);
$font = '/var/www/fonts/arial.ttf';
for ($i = 0; $i < $length; $i++) {
$color = imagecolorallocate($img, rand(0, 120), rand(0, 120), rand(0, 120));
$angle = rand(-15, 15);
$x = 15 + $i * 25;
$y = rand(30, 40);
imagettftext($img, 20, $angle, $x, $y, $color, $font, $code[$i]);
}
header('Content-type: image/png');
imagepng($img);
imagedestroy($img);
?>
三、增加干扰元素提升识别难度
仅更换字符集还不够,机器仍可能通过像素分析识别。我们可以在画布上绘制随机干扰线、噪点,或者对整个图像做轻微扭曲。干扰线使用imageline,噪点则遍历像素点随机填色。这些操作都在输出前完成,不影响session中的答案。
下面的代码在前面基础上增加了十条干扰线和一百个噪点。干扰线颜色与文字相近但更浅,目的是打破字符轮廓连续性。实践里干扰密度要适度,否则正常人也难以辨认,反而影响转化。
<?php
session_start();
$pool = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$code = substr(str_shuffle($pool), 0, 4);
$_SESSION['code'] = $code;
$width = 120; $height = 50;
$img = imagecreatetruecolor($width, $height);
imagefill($img, 0, 0, imagecolorallocate($img, 245, 245, 245));
// 干扰线
for ($i = 0; $i < 10; $i++) {
$c = imagecolorallocate($img, rand(150, 200), rand(150, 200), rand(150, 200));
imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $c);
}
// 噪点
for ($i = 0; $i < 100; $i++) {
$c = imagecolorallocate($img, rand(0, 255), rand(0, 255), rand(0, 255));
imagesetpixel($img, rand(0, $width), rand(0, $height), $c);
}
// 写文字
$font = '/var/www/fonts/arial.ttf';
for ($i = 0; $i < 4; $i++) {
$color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100));
imagettftext($img, 22, rand(-20, 20), 15 + $i * 25, rand(30, 45), $color, $font, $code[$i]);
}
header('Content-type: image/png');
imagepng($img);
imagedestroy($img);
?>
四、校验与过期规则配合修改
生成规则改完,校验端也要同步。一般我们在session中不仅存字符串,还存生成时间,用来控制五分钟失效。修改规则时建议把$_SESSION['code_time']一并写入,避免旧验证码长期有效被重放。
表单提交后,先用time()减去存储时间判断有效期,再做大小写统一比较。若失败则清除session中的验证码,防止暴力重试。这样整套规则从生成、展示到校验都可控,且无需引入额外扩展。
<?php
session_start();
if (!isset($_SESSION['code']) || !isset($_SESSION['code_time'])) {
echo '验证码无效';
exit;
}
if (time() - $_SESSION['code_time'] > 300) {
echo '验证码过期';
unset($_SESSION['code'], $_SESSION['code_time']);
exit;
}
if (strcasecmp($_SESSION['code'], $_POST['code']) !== 0) {
echo '验证码错误';
unset($_SESSION['code'], $_SESSION['code_time']);
exit;
}
echo '验证通过';
?>
五、不同干扰策略的对比
我们将三类常见做法放在一张表中比较,方便按场景选择。轻量干扰适合内部系统,强干扰适合公开注册页。注意GD库版本差异可能导致imagettftext字体路径解析不同,部署前要实测。
| 策略 | 字符集 | 干扰方式 | 识别难度 | 用户体验 |
|---|---|---|---|---|
| 默认数字 | 0-9 | 无 | 低 | 好 |
| 混合字符 | 去混淆字母数字 | 随机角度 | 中 | 较好 |
| 强干扰 | 去混淆字母数字 | 线、点、扭曲 | 高 | 一般 |
总结来说,PHP验证码生成规则的修改集中在字符池定义、GD绘图参数与session存储结构三点。理顺这条链路后,你可以自由组合字体、颜色与噪点,构建符合业务安全要求的验证机制。