不管是自己动手实现两步验证登录,还是换手机后想用备份密钥恢复 Google Authenticator,最容易让人抓狂的就是:算出来的 6 位码,和服务端要的就是对不上。TOTP 的迷人之处在于双方不通信也能验证同一个码,但这也意味着任何一个参数错位都会导致完全不同的结果、且没有任何报错提示。这篇先讲清它怎么算,再把对不上的原因按概率排查掉。
TOTP 是怎么算出来的
TOTP(基于时间的一次性口令,RFC 6238)的全过程其实只有四步:
- Base32 解码 密钥,得到二进制 key
- 取 当前 Unix 时间 ÷ 周期(默认 30 秒)向下取整,作为计数器 counter
- 用 key 对 counter 做 HMAC-SHA1(或 SHA256/SHA512)
- 对 HMAC 结果做动态截断,取末几位十进制即为验证码
关键在第 2 步:counter 来自时间,所以同一个 30 秒窗口内你和服务端算出的 counter 相同、码相同;窗口一过 counter 加一、码换新。双方共享的只是那个静态密钥,验证时各算各的、无需联网——这就是为什么验证器离线也能用。理解了这条链路,排查就有了方向:密钥、时间、算法参数,任何一环错位,码都不对。
坑一:设备时间不准(最高频)
因为 counter 直接由时间分片而来,本机时钟偏差是验证码对不上的头号原因。时间快或慢超过一个时间步(30 秒),算出的 counter 就和服务端差一格,码彻底不同,而且没有任何”时间错了”的提示。
- 解决:打开系统”自动设置时间 / 自动对时”,与 NTP 服务器同步
- 验证器 App(如 Google Authenticator)也有”时间校正”选项,本质是和 Google 的时间服务器对一次时间偏移
服务端为了容错,通常会接受前后各一个窗口(±30 秒)的码,但这只能容忍轻微漂移,本地时钟差几分钟照样失败。
坑二:算法、位数、周期不一致
TOTP 有三个可调参数,默认值是 SHA1 / 6 位 / 30 秒(绝大多数验证器和服务都按这套),但服务端完全可以改:
| 参数 | 默认 | 可选 | 不一致的后果 |
|---|---|---|---|
| 算法 | SHA-1 | SHA-256 / SHA-512 | 码完全不同 |
| 位数 | 6 | 7 / 8 | 长度和数值都不同 |
| 周期 | 30 秒 | 15 / 60 秒 | 分片错位、码不同 |
只要有一项不符,码就对不上。如果对方给了标准的 otpauth:// 链接,里面的 digits、period、algorithm 参数会被工具自动解析,照着填最稳;没有链接就逐项问清服务端配置。
坑三:密钥不是 Base32
TOTP 密钥按 Base32(RFC 4648)编码,字符集只有 A–Z 和 2–7,大小写不敏感、空格和短横线会被忽略。常见误区:
- 把十六进制密钥当 Base32 填(Hex 含 0、1、8、9 和 a–f,Base32 里没有这些数字)
- 复制时带进了多余字符、换行或把展示用的分组空格当成了密钥的一部分(其实空格会被自动忽略,整段粘贴即可)
拿到密钥先看字符集:只要出现 0、1、8、9 或小写 a/f 等 Base32 之外的字符,基本可以断定它不是 Base32,要回头确认来源或编码方式。
备份密钥与跨设备恢复
很多人换手机后才发现验证器里的码全没了——因为 2FA 绑定的是密钥,不是 App。每次开启两步验证时,屏幕上那串字母(或二维码里 otpauth 链接的 secret)就是密钥,务必离线备份原始密钥或服务给的恢复码,而不是只截个验证器界面的图。
只要留存了密钥,任何设备上输入它、参数设一致,就能重新算出当前码登录,再到账户安全页重新绑定新设备。临时应急、或验证自己实现的服务端逻辑时,本工具就是这样一个”有密钥就能出码”的地方——但密钥是长期凭证,别在公共设备粘贴真实账户的密钥。
小结
验证码对不上,几乎逃不出三类:设备时间不准、算法/位数/周期不一致、密钥不是 Base32。按这个顺序排查——先校时、再核参数、后查密钥编码——基本都能解决。理解 TOTP”密钥 + 时间 → 动态码”的本质,既能快速定位联调问题,也能在换设备时靠备份密钥从容恢复 2FA。