最稳方案是用 Overtrue\EasySms + 自定义缓存键:显式设 TTL、分离 sms: 与 sms_sent: 键、密钥禁放 .env、校验时判 null 而非 false、强转字符串防前置零丢失。

直接用 Overtrue\EasySms + 自定义缓存键是最稳、最可控的方案,别碰那些封装过深的“一键验证”包——它们在 Laravel 12 的队列上下文、多守卫或自定义用户表结构下容易丢状态、错生命周期。
为什么不用 laravel-mobile-verification 这类扩展包
它强制要求用户模型实现 MustVerifyMobile 接口并挂载对应 trait,但实际项目里你很可能已有自己的用户模型逻辑(比如多角色、软删除字段冲突、手机号非主键),强行套用会导致:
-
phone_verified_at字段被硬编码进迁移,无法适配已存在的表结构 - 缓存键名固定为
mobile_verification:{$phone},和你其他业务(如登录限流、密码重置)的键名规则不统一,后期排查难 - 发码逻辑耦合了事件派发,但没提供异步失败后的重试钩子,短信网关抖动时用户收不到码却无感知
- 不支持 Laravel 12 新增的
Illuminate\Contracts\Auth\CanResetPassword兼容层,升级时要重写验证流程
Cache::put('sms:'.$phone, $code, 300) 必须显式写死过期时间
别依赖缓存驱动默认 TTL。Redis 驱动下 Cache::put('sms:13812345678', '123456') 可能因配置缺失变成永不过期;而 File 或 Array 驱动则可能根本没设过期逻辑,导致验证码永远有效。
正确做法是始终显式传入秒数:
Cache::put('sms:'.$phone, $code, 300); // 5 分钟
Cache::put('sms_sent:'.$phone, true, 60); // 发送频控:1 分钟内不能再发
注意两个键必须分离——sms: 用于校验,sms_sent: 用于限流,混用会引发并发重复发送。
EasySms 网关配置必须抽到 config/sms.php 且禁用 .env 明文密钥
阿里云/腾讯云的 access_key_id 和 access_key_secret 绝不能直接写进 .env。Laravel 12 默认开启 APP_DEBUG=false 时,异常堆栈不会泄露 env,但开发阶段一旦误开 debug,密钥就裸奔。
安全做法是:
- 在
config/sms.php中只保留网关策略、超时、默认驱动等非敏感配置 - 把
access_key_id和secret放进服务器环境变量(如 systemd service 的Environment=ALIYUN_SMS_KEY=xxx) - 在配置文件中用
$_SERVER['ALIYUN_SMS_KEY'] ?? env('ALIYUN_SMS_KEY')回退读取 - 发码前加一层
if (empty($key)) { throw new RuntimeException('SMS key missing'); },避免静默失败
验证码校验时 Cache::get() 返回 null 就算失败,别用 === false 判断
这是高频翻车点:Cache::get('sms:13812345678') 在键不存在或已过期时返回 null,不是 false。若你写成:
if (Cache::get('sms:'.$phone) === false || Cache::get('sms:'.$phone) !== $inputCode) { ... }
会导致「验证码已过期」被当成「输入错误」提示用户重输,实际该引导重新获取。
正确逻辑是:
$cached = Cache::get('sms:'.$phone);
if ($cached === null) {
return response(['message' => '验证码已过期,请重新获取'], 400);
}
if ((string) $cached !== (string) $inputCode) {
return response(['message' => '验证码错误'], 400);
}
字符串强转是为了防数字型验证码开头为 0(如 012345)被 PHP 自动转成 12345 导致比对失败。
真正麻烦的从来不是发码,而是发完之后那 5 分钟里各种边界:用户切后台再回来、验证码被短信过滤器吞掉、双卡手机收错 SIM 卡、甚至运营商把 6 位纯数字当垃圾短信折叠——这些没法靠包解决,得靠日志埋点+缓存键可追溯+前端倒计时同步服务端剩余时间。











