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

















