2FA 是什么:定义、作用与 TOTP 的实现原理
密码只有一个失效模式:一旦泄露,它和账号就同时失效。两步验证(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 位 | 结论 |
|---|---|---|---|
| 59 | 94287082 | 287082 | 一致 |
| 1111111109 | 07081804 | 081804 | 一致 |
| 1111111111 | 050471 | 050471 | 一致 |
| 1234567890 | 89005924 | 005924 | 一致 |
四个向量全中,说明 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 的完整注册与断言流程。