照着做一遍,你就能得到一个「PHP 生成验证码 + 前端点击刷新 + 提交校验」能直接跑起来的完整实现,包括无 GD 环境下的降级方案。
第一步:确认环境,GD 扩展必须有
验证码图片靠 GD 库画出来,先确认扩展是否可用:
php -m | grep -i gd
能看到 gd 就说明可用。宝塔面板的话在「软件商店 → PHP → 设置 → 安装扩展」里装 gd。装上之后重启 PHP。
注意:没有 GD 不代表验证码功能就得砍掉,用算术题降级即可(第六步会写)。Clara BBS 就是这么处理的——后台「系统设置 → 安全」里可以开关图形验证码,检测不到 GD 时自动降级成「3 + 4 = ?」这类算术题,前端刷新逻辑完全一样。
第二步:写生成验证码的接口 captcha.php
新建 captcha.php,负责随机出码、存 Session、输出 PNG:
<?php
session_start();
header('Content-Type: image/png');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
// 去掉 0 O 1 l I 这类易混字符
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$code = '';
for ($i = 0; $i < 4; $i++) {
$code .= $chars[random_int(0, strlen($chars) - 1)];
}
$_SESSION['captcha_code'] = $code;
$_SESSION['captcha_time'] = time();
$w = 120; $h = 40;
$img = imagecreatetruecolor($w, $h);
imagefill($img, 0, 0, imagecolorallocate($img, 245, 247, 250));
// 干扰线 5 条
for ($i = 0; $i < 5; $i++) {
$c = imagecolorallocate($img, random_int(150, 220), random_int(150, 220), random_int(150, 220));
imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $c);
}
// 噪点 80 个
for ($i = 0; $i < 80; $i++) {
$c = imagecolorallocate($img, random_int(120, 230), random_int(120, 230), random_int(120, 230));
imagesetpixel($img, random_int(0, $w), random_int(0, $h), $c);
}
// 逐个字符随机位置、随机角度
$font = __DIR__ . '/font.ttf';
for ($i = 0; $i < 4; $i++) {
$c = imagecolorallocate($img, random_int(0, 120), random_int(0, 120), random_int(0, 120));
$x = 15 + $i * 25;
if (is_file($font) && function_exists('imagettftext')) {
imagettftext($img, 20, random_int(-20, 20), $x, random_int(30, 36), $c, $font, $code[$i]);
} else {
imagestring($img, 5, $x, 12, $code[$i], $c); // 无字体文件时兜底
}
}
imagepng($img);
imagedestroy($img);
注意:session_start() 必须放在任何输出之前,文件要存成 UTF-8 无 BOM。开头多一个空格或 BOM,图片就会输出成乱码。
第三步:前端展示图片 + 点击刷新
HTML 部分:
<img id="captchaImg" src="/captcha.php" alt="点击刷新" width="120" height="40" style="cursor:pointer">
<input type="text" name="captcha" maxlength="4" autocomplete="off" placeholder="验证码">
JS 部分,核心是给 URL 加时间戳:
const img = document.getElementById('captchaImg');
img.addEventListener('click', () => {
img.src = '/captcha.php?t=' + Date.now();
});
注意:不加 ?t= 时间戳的话,浏览器会认为 URL 没变,直接读缓存,点了没反应。如果站点挂了 CDN,还要对 captcha.php 单独设置不缓存,光靠响应头不一定够。
第四步:提交时校验
<?php
session_start();
$input = strtoupper(trim($_POST['captcha'] ?? ''));
$saved = $_SESSION['captcha_code'] ?? '';
$time = $_SESSION['captcha_time'] ?? 0;
unset($_SESSION['captcha_code'], $_SESSION['captcha_time']); // 一次性,立即销毁
if ($saved === '' || $input !== $saved) {
exit('验证码错误');
}
if (time() - $time > 300) {
exit('验证码已过期');
}
注意:校验完立刻 unset 是防暴力破解的关键。不然攻击者拿同一个 Session 反复提交,等于验证码形同虚设。生成时统一用大写,比对前用 strtoupper 归一,避免大小写折腾用户。
第五步:AJAX 提交失败后自动换一张
用 fetch 提交时,接口返回失败要顺手刷新图片,否则用户看到的还是刚才那张:
const res = await fetch('/post.php', { method: 'POST', body: formData });
const data = await res.json();
if (!data.ok) {
img.src = '/captcha.php?t=' + Date.now();
alert(data.msg);
}
注意:img.src 要用绝对路径(以 / 开头),相对路径在子目录页面下会拼错地址,刷新直接 404。
第六步:无 GD 环境的降级写法
在 captcha.php 最前面加个判断,画不了图就出算术题:
if (!function_exists('imagecreatetruecolor')) {
$a = random_int(1, 9); $b = random_int(1, 9);
$_SESSION['captcha_code'] = (string)($a + $b);
$_SESSION['captcha_time'] = time();
// 这里改为返回 JSON,前端把图片换成文字题
header('Content-Type: application/json');
echo json_encode(['question' => "$a + $b = ?"]);
exit;
}
前端拿到 question 就把 <img> 换成一行文字。刷新逻辑不变,照样加时间戳。
小结
- 生成端:
session_start() 打头,随机码写 Session,同时记时间戳,用 GD 画图输出 PNG;
- 刷新端:绑定 click 事件,用
?t=Date.now() 强制绕过缓存,路径用绝对地址;
- 校验端:比对前
strtoupper 归一,校验后立即 unset,再加个 5 分钟过期判断;
- 兼容端:用
function_exists('imagecreatetruecolor') 探测 GD,缺失时降级成算术题;
- 常见坑:BOM/空格导致图片乱码、缓存导致点击无反应、验证码可重复使用。