图形验证码与短信验证码必须分离校验:图形码依赖session单次存储(build后立即set captcha值,校验后delete),短信码须用Cache::put('sms:'.$phone, $code, 300)显式控制过期,键名、驱动、生命周期均不可混用。

Webman 里图形验证码和短信验证码不能共用一套校验逻辑,否则 session 和缓存键名冲突、生命周期错位、重放风险都会立刻暴露。图形码必须走 session(且依赖 CaptchaBuilder->getPhrase() 的单次性),短信码必须走 Cache::put('sms:'.$phone, $code, 300) 显式控制过期——这是两条完全不同的路径。
为什么 CaptchaBuilder->getPhrase() 只能调一次就变空
这不是 bug,是 Gregwar/Captcha 的设计机制:CaptchaBuilder 内部生成 phrase 后立即清空缓存,第二次调 getPhrase() 返回空字符串。不立刻存 session 就等于丢答案。
-
$builder->build()后必须紧跟着$request->session()->set('captcha', strtolower($builder->getPhrase())) - 绝不能在登录校验时再调
getPhrase()—— 此时值已销毁 - 多 worker 场景下,
file驱动的 session 会因进程隔离失效,必须切到redis驱动
/login/captcha 接口返回乱码或空白图片的排查点
GD 输出对响应体零容忍:任何 warning、notice、echo、空格、BOM 或额外 header 都会让浏览器解析失败,显示为乱码或触发下载。
- 控制器方法里禁止
echo/var_dump/print_r等调试输出 - 关掉 PHP 错误显示:
ini_set('display_errors', 'Off') - 强制指定响应头:
['Content-Type' => 'image/png'](别用 jpeg,GD 默认可能输出 png) - 用
curl -v http://127.0.0.1:8787/login/captcha | head -c 10检查开头是否为‰PNG;不是就说明有前置输出
短信验证码防刷与校验键名必须严格一致
发码成功但校验总失败?90% 是缓存键名拼写不一致,比如存的是 sms:138xxxx,取的时候却用了 phone:138xxxx 或 code_138xxxx。
立即学习“PHP免费学习笔记(深入)”;
- 发码前先限流:
if (Cache::has('sms_sent:'.$phone)) { return response(['msg' => '请稍后再试'], 429); } - 发码成功后写两份缓存:
Cache::put('sms:'.$phone, $code, 300)(校验用)、Cache::put('sms_sent:'.$phone, true, 60)(限流用) - 校验时必须用完全相同的 key:
Cache::get('sms:'.$phone),大小写、冒号、前缀一个字符都不能差 - 别用数据库存待验证短信码——高并发下容易锁表,缓存更轻量也更准
刷新验证码后旧值仍能通过校验的根本原因
Webman 的 CaptchaBuilder 不自带自动失效机制。“刷新”只是生成新图,但旧 session 值还在,用户输旧码仍能比对成功——这是典型的设计遗漏,不是配置问题。
- 刷新接口(如
GET /login/captcha/refresh)中,必须先删旧值:$request->session()->delete('captcha') - 生成新图后立刻 set 新值,顺序不能颠倒
- 若页面含多个表单(如注册+登录),不要复用固定 key
'captcha',改用带随机后缀的 key,例如'captcha_'.uniqid() - 校验成功后,立即
$request->session()->delete('captcha'),防止重放
图形码和短信码的存储位置、读取时机、过期策略、并发控制全部不同,强行合并校验逻辑只会让问题延迟爆发。最易被忽略的是:图形码的 session 必须由 Redis 统一驱动,而短信码的缓存 key 必须全链路大小写与分隔符严格一致——差一个字符,就永远校验不过。



















