Laravel ≥9.25 原生支持 Google Authenticator,需三字段(two_factor_secret、two_factor_recovery_codes、two_factor_confirmed_at)齐全且正确配置,verifyKey() 必须传 $window=1,恢复码为加密 JSON 格式,two_factor_confirmed_at 为空则校验被拦截。
直接用 Laravel 原生的
trait 就能跑通 Google Authenticator,不需要额外装第三方包——前提是你的 Laravel 版本 ≥ 9.25(对应
v9.25.0+),且已启用 Sanctum 或 Session Guard。
确认 Laravel 版本是否支持原生两步验证
Laravel 在 9.25 版本中正式将
trait 和配套数据库字段、验证逻辑合并进核心,此前版本(如 8.x 或 9.0–9.24)调用
会报错
。
运行
查看实际版本号
若低于 9.25,必须升级框架,或退而使用
包(注意它不兼容 Laravel 10 的 PHP 8.2+ 强类型约束,需锁定
)
原生实现依赖
和
,低版本手动复制 trait 无法补全底层校验逻辑
数据库迁移必须包含 three 个字段
只加
是不够的,
和状态判断会直接失败。Laravel 原生验证流程严格检查以下三字段是否存在且可写:
:TEXT 类型,存储 Base32 编码后的密钥(如
),不能为 NULL
:TEXT 类型,存 JSON 编码的加密数组(如
),不是明文逗号分隔
:TIMESTAMP NULLABLE,用于区分“已启用但未确认”和“已启用且已确认”两种状态
漏掉任意一个,调用
会静默失败,前端始终卡在“扫描二维码后点击确认”步骤。
失败的常见真实原因
用户输入 6 位验证码却提示
,大概率不是算法问题,而是上下文断开或参数错配:
没传
参数:原生
默认
,即只校验当前时间窗口(±0 秒),服务器与手机时钟偏差 >30 秒就必然失败;生产环境务必显式传
或
Session 未启动或已过期:TOTP 校验前需先通过密码登录并保持有效 session,否则
为空,
内部直接返回 false
密钥被二次 base64_decode:
字段内容是 Base32 编码字符串,不是 Base64;如果中间层(如 API 网关或日志脱敏)误做 Base64 解码再存库,会导致密钥损坏
恢复码校验失败别急着重输
报
时,先查
字段值是否为空或 JSON 格式错误(比如被手动编辑成明文字符串),再确认是否已用过该码:
原生逻辑每次成功调用
后,会从 JSON 数组中
移除该码
并重写整个字段;如果数据库字段被其他进程并发修改,可能造成数组丢失
不要用
替换原生生成的恢复码——前者返回明文数组,后者存的是加密 JSON,混用会导致
报错后整个校验流程中断
用户重新启用 2FA 时,旧恢复码自动失效,但前端可能还缓存着旧页面;需强制跳转到新设置页并清空本地 storage
最隐蔽的问题是:
为 NULL 时,即使恢复码正确,
也会返回 false,导致中间件直接拦截请求不进校验逻辑。
TwoFactorAuthenticatablelaravel/frameworkTwoFactorAuthenticatable$user->enableTwoFactorAuthentication()Method enableTwoFactorAuthentication does not existcomposer show laravel/frameworkpragmarx/google2fa-laravelv6.0.0Illuminate\Auth\Passwords\PasswordBrokerManagerIlluminate\Support\InteractsWithTimetwo_factor_secretuseRecoveryCode()two_factor_secretJBSWY3DPEHPK3PXPtwo_factor_recovery_codes"{\"0\":\"xk9m2v8q\",\"1\":\"t7r4f1p6\"}"two_factor_confirmed_at$user->confirmTwoFactorAuthentication()verifyKey()Invalid verification code$window = 1verifyKey()$window = 012Auth::user()verifyKey()two_factor_secretInvalid recovery codetwo_factor_recovery_codesuseRecoveryCode()Google2FA::generateBackupCodes()json_decode()two_factor_confirmed_atcanUseTwoFactor()