两步验证 TOTP 原理:6 位验证码怎么来的
一、一句话原理
验证码 = HMAC(密钥, floor(当前时间 ÷ 30秒)) 取 6 位
两步验证(2FA)里的动态验证码叫 TOTP(基于时间的一次性密码),核心是三个要素:
| 要素 | 说明 |
|---|---|
| 共享密钥 | 注册时服务器给你的那串 Base32 字符 |
| 时间片 | 默认 30 秒一片,把时间切成计数器 |
| HMAC 算法 | 默认 SHA-1,也有 SHA-256 |
服务器和你的 App 各算一遍,对得上就通过。没有任何网络交互——这正是它离线也能用的原因。
二、算一遍:计数器与验证码
以时间戳 1789000000(epoch 秒)为例:
counter = floor(1789000000 ÷ 30) = 59633333
密钥 JBSWY3DPEHPK3PXP(16 个 Base32 字符 = 10 字节)在该时间片的验证码:
| 参数 | 验证码 | 下一个 |
|---|---|---|
| 默认 6 位 SHA-1 | 159232 | 274417 |
| 8 位 | 06159232 | 67274417 |
| SHA-256 | 133128 | 717031 |
换成 32 字符的密钥,解出来是 20 字节,验证码也不同(实测 122335)。密钥越长,解出的码字不同——所以复制密钥时多一位少一位都会失效。
顺带说说 HMAC:HMAC-SHA1("hello", "key") = b34ceac4516ff23a143e61d79d0fa7a4fbe5f266。TOTP 就是把「计数器」当消息做一次 HMAC,再从结果里截取 6 位十进制数。
三、Base32:密钥为什么只认这些字符
Base32 的字符集只有 A~Z 和 2~7(32 个),另外允许空格、连字符与 = 填充(工具会自动忽略)。
所以密钥里不会出现数字 0 和 1:
- 看到
0→ 大概率是把字母 O 看错了; - 看到
1→ 大概率是把字母 I 看错了。
工具遇到这类非法字符会直接报「密钥不是合法 Base32」,而不是硬算一个错误结果。常见错因还有:
- 把
otpauth://totp/...整条链接粘进来(斜杠与冒号不是 Base32 字符); - 少复制或多复制了几位;
- 从二维码解析结果里复制时带了多余空格。
正确做法:只填 secret 参数那一部分。
四、验证码为什么总是不对
按出现频率排序:
- 手机时间不准(第一大原因)。TOTP 依赖时间,差 30 秒以上必然算错。手机开「自动设置时间」即可;服务器端同理,运维手册里通常要求 NTP 对时;
- 时间边界效应:验证码在第 30 秒的瞬间切换。本机还剩 0.2 秒时提交、服务器收到时已进入下一片,就会出现「刚生成就失效」。稳妥做法是等进度条回满(新时间片刚开始)再提交;部分服务端也允许前后各一个时间片,容忍这种抖动;
- 密钥抄错(见上一节);
- 设备时间对但时区错:TOTP 用的是 epoch 时间(UTC 基准),时区设置错误在极端情况下会连带影响系统时间。
一天有 86400 ÷ 30 = 2880 个时间片,也就是理论上一天最多会有 2880 个不同的验证码——但这些码都是同一个密钥算出来的,真正的秘密始终是那串 Base32 密钥。
五、安全边界
- 密钥等价于密码:拿到密钥的人可以无限生成验证码,所以不要截图、不要粘到聊天窗口。用这个工具算验证码是安全的(纯本地),但生产环境的密钥不建议粘到任何在线页面;
- TOTP 防的是「密码泄露」,防不了中间人钓鱼(假登录页可以实时转发验证码);
- 比短信验证码更安全:短信可被 SIM 卡劫持与拦截,TOTP 不依赖运营商;
- 备份码要存好:换手机时若没备份码,很多账户会走繁琐的人工申诉。
六、直接算
→ TOTP 验证码生成器:按密钥生成当前与下一个验证码,本地计算
→ HMAC 计算器:HMAC-SHA1/SHA256,理解 TOTP 的底层算法
→ Base32 编解码:密钥的编码格式与合法字符范围
本站文章为个人使用经验的原创整理,除已注明来源的引用外均为本人撰写。文中涉及的软件名称、商标、截图等归各自权利人所有,引用仅用于说明与交流。如认为本站内容侵犯了你的合法权益,请通过「关于本站」页面的联系方式告知,核实后将及时更正或删除。