短信验证码失败主因是环境与调用链问题:Session未启动、跨子域ID不一致、验证码被消耗;应禁用session存储,改用手机号绑定的缓存键+过期时间控制,并统一处理前后端大小写与空格。

ThinkPHP 实现短信验证码,核心是「生成 + 存储 + 发送 + 校验」四步闭环,但最容易卡在 session 不一致、access_key_secret 泄露、或 check() 永远返回 false 上 —— 这些不是代码写错了,而是环境或调用链断了。
为什么 captcha_check() 总是 false?先查这三件事
90% 的「验证码校验失败」和逻辑无关,而是底层支撑没跑通:
-
session_start()没生效:TP6 默认注释了\think\middleware\SessionInit::class,必须在app/middleware.php中取消注释;仅靠session('key', 'val')赋值不等于 Session 已启动 - 前后端 Session ID 不一致:比如前端请求域名是
www.example.com,而验证码接口走的是api.example.com,跨子域导致 session_id() 完全不同,存一份、查另一份 - 验证码已被消耗:
captcha_check()成功后自动从 session 中清除原始值,若页面 JS 重复提交(如按钮未禁用)、或用户刷新后重填旧值,必然失败;建议校验前先session('captcha')打印看是否存在
发送短信必须用扩展包,别手写 curl
直接用 curl_init() 或 file_get_contents() 调阿里云/云片接口,看似简单,实则埋雷:
- SSL 证书验证失败(尤其测试环境):
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)是临时解法,但上线必须关掉 - 缺少重试与熔断:网络抖动时请求直接丢,用户点一次没反应,再点又触发新验证码,造成超发
- 密钥硬编码风险:把
access_key_secret写死在控制器里,一不小心提交到 GitHub 就等于送钥匙
推荐用社区维护的 overtrue/easy-sms:composer require overtrue/easy-sms,配置统一收口在 config/sms.php,密钥从环境变量读取(env('ALIYUN_ACCESS_KEY_SECRET')),发送失败可 catch 异常并记录日志。
立即学习“PHP免费学习笔记(深入)”;
验证码存储必须绑定手机号,不能只存 code
很多实现把验证码存在 session('sms_code', $code),看似省事,但一上线就出问题:
- 同一设备多个手机号切换时,旧验证码残留,A 用户输 B 用户的手机号+自己的验证码也能过
- 用户换浏览器或清缓存,Session 失效,但短信已发出,无法二次校验
- 没有过期时间控制:Session 默认存活 24 小时,而短信验证码通常只允许 5 分钟有效
正确做法是用缓存键绑定手机号:cache('sms_code_' . $mobile, $code, 300)(单位秒),发送前先检查是否已有未过期记录,避免高频刷码;校验时也按此 key 读取,和 Session 解耦。
前端传参不 trim、不分大小写,后端就得兜底
用户输入框里粘贴验证码,末尾常带空格;手机键盘默认首字母大写,但验证码图可能是小写。这些细节不处理,captcha_check() 就会因严格比对失败:
- 前端提交前加
.trim().toUpperCase()(JS)或后端用trim($input)+strtoupper($input) - 如果验证码配置里
'useZh' => false(禁用中文),但字体文件缺失,可能 fallback 到空图,create()返回空字符串 —— 此时要检查public/fonts/下是否有可用字体,或改用系统默认字体路径 - 图像输出被意外截断:控制器方法里任何
echo、var_dump、甚至模板里多一个换行,都会让 PNG 流损坏,浏览器显示“破损图像”;务必确保验证码方法里只有Captcha::create()或captcha_img()
最易被忽略的一点:短信验证码的「时效性」和「唯一性」必须由缓存层保证,而不是靠 Session 生命周期或前端倒计时。哪怕用户关掉页面再打开,只要没超时、没重发,原验证码依然有效 —— 这才是真实场景下的健壮设计。



















