KernelRanch
返回文章列表

2FA 是什么:定义、作用与 TOTP 的实现原理

2026-11-10 #2fa#totp#security#authentication#rfc6238

密码只有一个失效模式:一旦泄露,它和账号就同时失效。两步验证(Two-Factor Authentication,2FA)要解决的就是这件事——让"知道密码"不再等于"能登录"。

1. 定义

2FA 要求登录时提供两类不同性质的凭据,类别通常是这三种:

类别例子性质
你知道的密码、PIN、安全问题可被记住,也可被窃取
你拥有的手机、验证器 App、硬件密钥物理持有,复制成本高
你是什么指纹、面容、虹膜生物特征,难以更换

任意两类组合即构成 2FA。关键在于"不同性质":密码加安全问题不算,因为两者同属"你知道的",一次钓鱼可以同时骗走。

2. 作用:把一次泄露降级为一次失败尝试

现实中账号失守的主因是撞库、钓鱼和第三方数据泄露,这三类都直接落在密码上。启用 2FA 之后,攻击者拿到了密码,仍然缺第二把钥匙。

它带来的变化是具体的:原本一次密码泄露等于账号被接管,现在变成一次会失败的登录尝试,而且你能在日志里看到这次尝试。安全上的价值不在于"更难猜",而在于"单点失守不再致命"。

3. 它挡不住什么

说清楚边界比夸它有用更重要。三种情况 2FA 基本无效:

会话劫持。你登录之后的 Cookie 被偷走,攻击者直接复用会话,根本不需要过验证码。

实时中间人钓鱼。假站点把你的账号密码和验证码实时转发给真站点,验证码在 30 秒内是有效的。

MFA 疲劳。攻击者连续推送登录确认,直到你手滑点了"同意"。

这三种的共同点是:攻击发生在"你已经通过验证之后"或"你主动配合了攻击者"。所以 2FA 是降低单点失守的损失,不是万能盾。

4. 原理:一个 HMAC 加一个时间片

最普及的形式是 TOTP(Time-based One-Time Password,RFC 6238)。服务器与你的验证器在绑定阶段共享一个密钥,之后双方各自计算:

counter = floor(unix_time / 30)
code    = H(HMAC-SHA1(secret, counter)) mod 10^6

H 是动态截断(dynamic truncation):取 HMAC 结果的最后一个字节的低 4 位作为偏移量,从该位置取 4 个字节,去掉最高位,再对 10^6 取模,左侧补零到 6 位。这样得到的数字分布在 000000–999999 之间。

整个过程没有网络交互,所以飞行模式下也能出码。这也是它比短信验证码可靠的地方:不依赖运营商,也不怕 SIM 卡被补办。

5. 四个实现细节决定安全性

时间片长度。默认 30 秒。太短会频繁失效,太长会扩大重放窗口。

时钟漂移。服务器通常接受前后各一个时间片,也就是 ±30 秒的容差。客户端实现里要按这个窗口做校验,而不是只比对当前值。

一次性。同一个时间片内的验证码用过一次就该作废,否则截获后可以重放。这一步很多自建系统漏掉了。

密钥分发。密钥只在绑定那一刻通过二维码(otpauth:// URI)传输,之后永远不离开设备。服务器端存的也只是这个共享密钥,不是验证码。

6. 一个可以自己验证的演示

我把这套逻辑做成了一个纯前端的演示站:页面在浏览器里用 WebCrypto 算出验证码,服务端用同一套算法提供 API,没有任何数据库。

为了确认实现不是"看起来能用",我用 RFC 6238 附录里的测试向量做了核对。密钥取 ASCII 12345678901234567890(Base32 为 GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ),取 8 位结果的低 6 位:

时间(秒)RFC 6238 的 8 位结果本站 API 返回的 6 位结论
5994287082287082一致
111111110907081804081804一致
1111111111050471050471一致
123456789089005924005924一致

四个向量全中,说明 HMAC-SHA1、动态截断和取模都实现对了。你可以直接把页面上的验证码拿去和任意 TOTP 验证器对比。

演示地址:https://quote.kernelranch.com/2fa

接口也很简单:GET /api/totp?secret=... 返回当前验证码与剩余秒数,GET /api/verify?secret=...&code=... 返回是否通过(前后各一个时间片内匹配即通过)。

7. 第二因素怎么选

方式抗钓鱼便利性建议
TOTP(验证器 App)一般高,离线可用大多数账号的默认选择
短信验证码差,可被 SIM 劫持最高只作兜底,不做唯一手段
硬件密钥(WebAuthn)强,绑定域名中,需随身携带邮箱、云账号、代码仓库优先
推送确认中高企业内部,注意防疲劳轰炸

一个务实的原则:主账号用硬件密钥,日常账号用 TOTP,短信只留作恢复通道,同时把恢复码离线保存。只在一个设备上绑定、丢了手机就永久失去账号,是真实发生过的场景。

8. 三个常见误区

把 2FA 当成"防止密码被猜中"。它防的是密码已经泄露之后的事,弱密码仍然该改。

把验证码发给别人。任何要你提供验证码的"客服"都是攻击者。

只绑定一个设备。恢复码没存、备用验证器没绑,等于把账号的命运交给一台手机。

小结

2FA 的核心不是多加一道工序,而是引入一个"不在密码库里"的因素。它把凭据泄露的后果从"账号丢失"降级为"一次失败的尝试"。

TOTP 之所以成为事实标准,是因为它足够简单:一个共享密钥、一个时间片、一次 HMAC,就能在完全离线的情况下算出双方一致的六位数字。简单到可以用几十行代码实现,也简单到能用 RFC 的测试向量验证对错。

想亲手试一次,可以打开 https://quote.kernelranch.com/2fa ,页面上的验证码是本地算的,断网也不会失效。

数据来源与检索时间

检索时间 2026-11-10。RFC 测试向量与 API 返回值为本机实测。

来源URL对应数据
RFC 6238《TOTP》https://www.rfc-editor.org/rfc/rfc6238.txt时间片 30 秒、HMAC-SHA1 构造、动态截断算法、附录 B 的测试向量(59→94287082、1111111109→07081804、1111111111→050471、1234567890→89005924)
OWASP《Multifactor Authentication Cheat Sheet》https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html第二因素分类、短信验证码的风险、MFA 疲劳与推送轰炸的防护建议
RFC 4226《HOTP》https://www.rfc-editor.org/rfc/rfc4226.txt动态截断与基于计数器的构造,TOTP 的基础

核验说明

  • 四个测试向量的比对是本机对已部署站点发起的真实请求(/api/totp?secret=...&t=...),返回的 code 与 RFC 值逐条核对一致。
  • 演示站部署在 Cloudflare Workers 上,域名 quote.kernelranch.com,HTTPS 已生效(/2fa 返回 200,响应体约 9.9KB);服务端与页面的算法是两份独立实现,页面那份在浏览器里跑。
  • 未做:没有做安全审计,没有测并发下的验证码重放防护(演示用,不存储状态);没有验证 WebAuthn 的完整注册与断言流程。