当你在登录GitHub、Google或者各类企业内网系统时,除了输入密码之外,往往还要求填一个六位数字验证码,这个验证码每隔三十秒变化一次,来源正是手机上的Authenticator验证器应用。支撑这类应用的核心技术就是TOTP(Time-based One-Time Password,基于时间的一次性密码算法)。理解TOTP的原理,不仅能帮助你在自己的系统中接入两步验证,也能让你更清楚地认识到动态口令的安全边界。

TOTP算法的核心原理是什么
TOTP并不是一个凭空出现的新算法,它构建在HOTP(HMAC-based One-Time Password)之上,由RFC 6238文档标准化。整个流程可以概括为三个要素:一个共享密钥、一个随时间推进的计数器、一个HMAC哈希函数。用户在绑定验证器时,服务端会生成一段随机密钥,通过二维码的形式交给用户手机保存,此后双方各自基于这把密钥进行计算,全程不再传输密钥本身。
计数器的计算方式很直接:用当前Unix时间戳除以时间步长(默认30秒),得到的整数就是计数器值。由于双方使用的时钟基本一致,服务端和手机端在同一个30秒窗口内会得到相同的计数器。接着将密钥和计数器作为输入,通过HMAC-SHA1算法生成一个固定长度的哈希值,再经过动态截断(Dynamic Truncation)从中取出4个字节,对1千万取模,最终得到一个六位数字。
import hmac
import hashlib
import struct
import time
def totp(secret: bytes, period: int = 30, digits: int = 6) -> str:
# 计算时间计数器:当前时间戳除以步长
counter = int(time.time()) // period
# 将计数器转为8字节大端整数
msg = struct.pack(">Q", counter)
# 计算HMAC-SHA1
digest = hmac.new(secret, msg, hashlib.sha1).digest()
# 动态截断:取最后一个字节的低4位作为偏移量
offset = digest[-1] & 0x0F
# 从偏移量处取4字节并抹去最高位
code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
# 取模得到指定位数的验证码
return str(code % (10 ** digits)).zfill(digits)这段二十行左右的代码就是TOTP的全部核心逻辑,可见算法本身并不复杂。之所以选择HMAC-SHA1而不是更强的SHA256,主要是历史兼容性考虑——大量早期的硬件令牌只支持SHA1,RFC 6238也允许使用SHA256和SHA512,但绝大多数验证器应用默认仍然使用SHA1。
如何生成密钥并让Authenticator应用完成绑定
服务端接入TOTP的第一步是生成共享密钥。密钥必须使用密码学安全的随机源产生,长度建议不少于160位(20字节)。生成的密钥需要经过Base32编码后展示给用户,因为Base32字符集只包含大写字母和数字2到7,避免了大小写歧义,方便用户手动输入。接下来将密钥封装成otpauth格式的URI,再渲染成二维码供Authenticator应用扫描。
otpauth URI的标准格式为otpauth://totp/标签?secret=密钥&issuer=发行方,其中标签通常写成“用户名@域名”的形式。Authenticator应用扫描二维码后会解析出密钥、发行方、账号以及周期等参数,并将其安全地存储在本地。下面给出一个基于Python的pyotp库的完整绑定流程示例。
import pyotp
import qrcode
# 生成随机Base32密钥(等价于20字节的随机数)
secret = pyotp.random_base32()
# 构造otpauth URI
uri = pyotp.totp.TOTP(secret).provisioning_uri(
name="alice@ippipp.com",
issuer_name="MyService"
)
# 生成二维码图片,用户用Authenticator扫描即可完成绑定
img = qrcode.make(uri)
img.save("binding_qrcode.png")
print("密钥(备用,可让用户手动输入):", secret)绑定完成后,服务端必须把密钥与用户账号关联并持久化保存。这里有一个重要的实践建议:密钥在数据库中应当加密存储,而不是明文落盘。一旦数据库被拖库,明文存储的TOTP密钥会让攻击者直接在自己的设备上生成有效验证码,两步验证形同虚设。常见的做法是使用KMS或者信封加密,密钥加密密钥保存在独立的安全模块中。
服务端如何校验验证码以及常见坑点
校验阶段服务端重复同样的计算过程,将用户提交的六位数字与本地计算结果比对。但直接做字符串相等比较是不够的,因为手机和服务器的时钟可能存在几十秒的偏差。标准做法是允许前后各一个时间窗口的误差,即同时校验上一个、当前和下一个窗口的验证码,只要任意一个匹配即通过。此外,比较时务必使用恒定时间比较函数,防止计时攻击。
import pyotp
from typing import Optional
def verify_totp(secret: str, code: str) -> bool:
totp = pyotp.TOTP(secret)
# valid_window=1 表示允许前后各一个30秒窗口的时间偏差
if totp.verify(code, valid_window=1):
return True
return False
# 注意:同一验证码通过后应立即标记为已使用,
# 防止攻击者在同一个30秒窗口内重放刚用过的验证码重放防范是最容易被忽略的一点。TOTP算法本身不记忆历史,同一个窗口内同一个验证码可以被无限次提交成功。正确的做法是在服务端记录最近一次验证成功的时间窗口编号,如果新提交的验证码对应的窗口不大于已记录值,就拒绝。大多数成熟库并不内置这个逻辑,需要开发者自行维护。
另一个高频问题是用户换手机后验证码丢失。由于Authenticator应用大多不联网同步密钥,建议在绑定时同时展示备份恢复码,每个恢复码只能使用一次,用户可以在丢失验证器时用它登录并重新绑定。此外,还应提供备用绑定渠道,例如通过短信或邮件验证身份后允许重置TOTP,否则用户一旦丢失手机就只能走成本极高的人工申诉流程。
最后要注意时间源的可靠性。服务器务必开启NTP时间同步,跨机房部署时尤其要检查各节点的时间一致性。如果服务器时间偏差超过一个时间窗口,所有用户的验证码都会校验失败,这类故障的表现非常隐蔽——用户输的码看起来完全正确,但系统始终提示验证码错误,排查时第一时间应该对比服务器时间与标准时间的差异。
总结来看,TOTP以极低的实现成本提供了远高于静态密码的安全强度,核心不过是共享密钥加时间计数器的HMAC运算。真正决定安全性的往往不是算法本身,而是密钥的存储方式、重放防护的完善程度以及丢失恢复机制的设计。把这些工程细节处理好,你的两步验证体系才能真正立得住。
TOTPAuthenticator两步验证修改时间:2026-09-01 15:44:39