两步验证 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-1159232274417
8 位0615923267274417
SHA-256133128717031

换成 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」,而不是硬算一个错误结果。常见错因还有:

  1. otpauth://totp/... 整条链接粘进来(斜杠与冒号不是 Base32 字符);
  2. 少复制或多复制了几位;
  3. 从二维码解析结果里复制时带了多余空格。

正确做法:只填 secret 参数那一部分。

四、验证码为什么总是不对

按出现频率排序:

  1. 手机时间不准(第一大原因)。TOTP 依赖时间,差 30 秒以上必然算错。手机开「自动设置时间」即可;服务器端同理,运维手册里通常要求 NTP 对时;
  2. 时间边界效应:验证码在第 30 秒的瞬间切换。本机还剩 0.2 秒时提交、服务器收到时已进入下一片,就会出现「刚生成就失效」。稳妥做法是等进度条回满(新时间片刚开始)再提交;部分服务端也允许前后各一个时间片,容忍这种抖动;
  3. 密钥抄错(见上一节);
  4. 设备时间对但时区错:TOTP 用的是 epoch 时间(UTC 基准),时区设置错误在极端情况下会连带影响系统时间。

一天有 86400 ÷ 30 = 2880 个时间片,也就是理论上一天最多会有 2880 个不同的验证码——但这些码都是同一个密钥算出来的,真正的秘密始终是那串 Base32 密钥

五、安全边界

  • 密钥等价于密码:拿到密钥的人可以无限生成验证码,所以不要截图、不要粘到聊天窗口。用这个工具算验证码是安全的(纯本地),但生产环境的密钥不建议粘到任何在线页面
  • TOTP 防的是「密码泄露」,防不了中间人钓鱼(假登录页可以实时转发验证码);
  • 比短信验证码更安全:短信可被 SIM 卡劫持与拦截,TOTP 不依赖运营商;
  • 备份码要存好:换手机时若没备份码,很多账户会走繁琐的人工申诉。

六、直接算

TOTP 验证码生成器:按密钥生成当前与下一个验证码,本地计算

HMAC 计算器:HMAC-SHA1/SHA256,理解 TOTP 的底层算法

Base32 编解码:密钥的编码格式与合法字符范围

本站文章为个人使用经验的原创整理,除已注明来源的引用外均为本人撰写。文中涉及的软件名称、商标、截图等归各自权利人所有,引用仅用于说明与交流。如认为本站内容侵犯了你的合法权益,请通过「关于本站」页面的联系方式告知,核实后将及时更正或删除。