Appearance
TL;DR
- 原理:Google/Microsoft 等验证器本质上都是 OATH 标准的 TOTP(基于时间的一次性密码),源自 RFC 6238(基于 HOTP/RFC 4226 扩展)。默认 30 秒一换码,6 位数字,使用 HMAC-SHA1/256/512 计算。(IETF Datatracker)
- 绑定:后端生成一个 Base32 密钥(secret),拼成
otpauth://URI 给前端展示为 二维码;用户用验证器扫二维码 → 验证器开始本地算码 → 用户回填一次性验证码到后端校验 → 校验通过即开启 2FA。不需要、也不应该再把 secret 发回后端(后端已经有)。(GitHub) - 实现:前端渲染二维码(例如 qrcode),后端用库(Node.js
otplib、Pythonpyotp、Javajava-otp)生成 secret、构造otpauth://、校验验证码并完成持久化与风控。(Otplib)
一、常见验证器与差异
- Google Authenticator:标准 TOTP,支持扫描
otpauth://,可选账号云同步(注意隐私权衡)。(Google 帮助) - Microsoft Authenticator:同样支持 OATH TOTP(也有推送批准与“数字匹配”功能,和 TOTP 是两种形态)。(Microsoft Learn)
这两款在“扫描二维码 → 本地计算一次性验证码”的 TOTP 场景是互通的。
二、工作原理(够用但准确)
- HOTP(事件计数):
code = Truncate(HMAC(K, counter)),每用一次计数+1。(IETF) - TOTP(时间计数):把计数器换成时间片
C = floor((now - T0)/period),常见period=30s,默认 6 位数字。支持 SHA1/256/512。(IETF Datatracker) - 密钥分发:用
otpauth://TYPE/Label?secret=...&issuer=...&period=30&digits=6&algorithm=SHA1,secret为 Base32 编码。验证器扫二维码后就能开始计算。(GitHub)
三、你描述的“绑定流程”有个小修正
“我把码和密钥发给后端完成绑定” —— 不建议这样做。
推荐安全流程(后端早已生成并持有 secret):
- 后端生成
secret(Base32),构造otpauth://,返回 二维码(或明文 secret 供手输)。 - 用户用验证器扫描 → 验证器本地算出 6 位码。
- 用户只回填一次性验证码(6 位码)到后端;
- 后端用已保存的 secret 验证这 6 位码(允许 ±1 时间片容差),通过则标记已绑定。
- 保存与保护 secret(加密存储),并发放恢复码、开启风控策略(节流、重试限制等)。 (规范与参数见 RFC/Key URI/OWASP)。(IETF Datatracker)
四、接口设计(REST 示例)
1)获取绑定二维码
POST /api/mfa/setup- 入参:需要登录态(谁要开 2FA)
- 返回:
json
{
"setupId": "uuid",
"otpauthUri": "otpauth://totp/YourApp:alice@example.com?secret=JBSW...&issuer=YourApp&period=30&digits=6&algorithm=SHA1",
"secret": "JBSW..." // 仅用于手动输入时显示,可视为敏感信息
}服务器同时把
setupId -> secret(加密)存为“待验证”状态。(GitHub)
2)确认绑定
POST /api/mfa/verify- 入参:
{ "setupId": "uuid", "code": "123456" } - 逻辑:根据
setupId取回 secret,用 TOTP 校验code(窗口 ±1),通过则把 secret 迁移到用户正式配置并标记 2FA 已启用。
3)登录二次校验
POST /api/mfa/challenge获取挑战态POST /api/mfa/challenge/verify入参{code},通过即颁发登录态/刷新期。
五、前端实现要点
- 二维码渲染:拿到
otpauthUri后用前端库(如qrcode)渲染为<img/>;失败时提供明文secret供手输。 - 时间同步:前端不参与计算(验证器自己会算),但要清晰的错误提示与重试节流。
- 隐私:二维码只在 HTTPS 下展示,避免 DOM 截图/日志泄露。
示例(React/TS):
ts
// 复杂逻辑:把 otpauth:// 转成 DataURL 并渲染二维码
import QRCode from "qrcode";
export async function otpauthToDataUrl(otpauthUri: string) {
// 复杂逻辑:qrcode 库异步生成 Base64 DataURL
return await QRCode.toDataURL(otpauthUri);
}六、后端实现(Node.js / TypeScript,基于 otplib)
库说明:
otplib同时支持 HOTP/TOTP,符合 RFC 4226/6238。(npm)
ts
// 复杂逻辑:生成 Base32 secret、构造 otpauth URI
import { authenticator } from "otplib";
// 可选:全局参数(30 秒步长,±1 个时间片容差)
authenticator.options = { step: 30, window: [1, 1] };
export function createMfaSetup(email: string, issuer = "YourApp") {
// 复杂逻辑:随机生成 Base32 secret(服务端保存加密后的密文)
const secret = authenticator.generateSecret(); // Base32
// 复杂逻辑:构造 otpauth:// URI(二维码内容)
const otpauth = authenticator.keyuri(email, issuer, secret);
return { secret, otpauth };
}
export function verifyMfaCode(secretBase32: string, code: string) {
// 复杂逻辑:用同一 secret 校验 6 位码(允许 ±1 时间片)
return authenticator.verify({ token: code, secret: secretBase32 });
}七、后端实现(Python / FastAPI,基于 PyOTP)
库说明:
pyotp用于生成与校验 TOTP。(pyauth.github.io)
py
# 复杂逻辑:生成 secret、构造 otpauth、校验验证码
import pyotp
def create_mfa_setup(email: str, issuer: str = "YourApp"):
# 复杂逻辑:随机 Base32 secret
secret = pyotp.random_base32()
# 复杂逻辑:构造 otpauth:// URI
otpauth = pyotp.totp.TOTP(secret).provisioning_uri(name=email, issuer_name=issuer)
return secret, otpauth
def verify_mfa_code(secret_base32: str, code: str) -> bool:
# 复杂逻辑:默认 30s 步长,可设置 valid_window=1 允许前后各一窗
return pyotp.TOTP(secret_base32).verify(code, valid_window=1)八、后端实现(Java / Spring Boot,基于 java-otp)
com.eatthepath:java-otp默认 30s 步长、6 位、HmacSHA1;如需 Base32 解码可用commons-codec。(JChambers)
java
// 复杂逻辑:用 java-otp 校验 TOTP(30s 步长,±1 时间片手动容忍)
import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator;
import org.apache.commons.codec.binary.Base32;
import javax.crypto.spec.SecretKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.security.Key;
public boolean verifyTotp(String base32Secret, String code) throws Exception {
// 复杂逻辑:Base32 -> key(算法与生成器一致,默认 HmacSHA1)
byte[] keyBytes = new Base32().decode(base32Secret);
Key key = new SecretKeySpec(keyBytes, "HmacSHA1");
TimeBasedOneTimePasswordGenerator totp =
new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30));
// 复杂逻辑:当前时间片生成一次性密码
int current = totp.generateOneTimePassword(key, Instant.now());
// 复杂逻辑:容忍前后各一窗(手动再算前/后时间片)
int prev = totp.generateOneTimePassword(key, Instant.now().minusSeconds(30));
int next = totp.generateOneTimePassword(key, Instant.now().plusSeconds(30));
return code.equals(String.format("%06d", current)) ||
code.equals(String.format("%06d", prev)) ||
code.equals(String.format("%06d", next));
}九、关键安全实践清单
- 不要把 secret 回传:后端生成并安全保存(建议加密存储,密钥托管/分区权限)。(OWASP Cheat Sheet Series)
- 加密传输:二维码与手输 secret 仅在 HTTPS 下展示/传输。
- 时间同步:服务器启用 NTP,校验窗口建议 ±1 时间片。(IETF Datatracker)
- 速率限制:验证码校验接口加 节流/黑名单(参考 OWASP 身份验证与 MFA 指南)。(OWASP Cheat Sheet Series)
- 恢复码:提供一次性恢复码(建议哈希存储);丢机/换机时用于解锁与重置。
- 最小展示:二维码只展示一次;需要重扫需重新申请。
- 微软差异:若采用 推送+数字匹配 是另一套流程;与 TOTP 并行存在,不同管控策略。(Microsoft Learn)
- 参数一致:前后端 period/digits/algorithm 必须一致(Key URI 可显式声明,默认 period=30、digits=6)。(GitHub)
十、与官方/标准对齐的“广泛调研”要点
- TOTP/HOTP 标准:RFC 6238(TOTP)、RFC 4226(HOTP)。(IETF Datatracker)
- Key URI(otpauth://):Base32
secret、issuer、digits、period等参数定义与默认值。(GitHub) - Google 文档:Authenticator 的 2 步验证与离线生成机制;近年新增“可选云同步”。(Google 帮助)
- Microsoft 文档:Authenticator 作为软件 OATH TOTP 令牌的说明;30s 周期;ASP.NET Core 生成二维码指南。(Microsoft Learn)
- 实现库:Node.js
otplib、Pythonpyotp、Javajava-otp。(npm) - OWASP:MFA 与机密管理实践。(OWASP Cheat Sheet Series)
十一、最简落地清单(按顺序做就能跑)
- 后端:选库(otplib/pyotp/java-otp),实现
createMfaSetup()+verifyMfaCode();密钥加密入库。(npm) - 前端:拿
otpauthUri用qrcode渲染二维码,提供“我已扫码,输入 6 位验证码”表单。 - 接口:
/mfa/setup(出二维码)→/mfa/verify(校验码,窗口 ±1);失败次数上限与冷却。 - 登录:若用户已开启 2FA,密码通过后进入
/mfa/challenge流程再校验一次码。 - 运维:NTP 校时;日志打点(不要记录 secret 与明文码);WAF/节流;恢复码与紧急解绑流程。
十二、FAQ
Q:为什么非要 Base32? A:因为
otpauth://约定用 Base32 传递共享密钥,兼容主流验证器(Google 等)。(GitHub)Q:能不能把 secret 只存在用户手机? A:TOTP 是对称方案,服务端必须持有同一 secret 才能独立计算并比对;若不想让服务器持有密钥,考虑 FIDO2/Passkeys 之类的非对称方案。
Q:微软“数字匹配”和 TOTP 的关系? A:数字匹配是推送验证的增强,与 TOTP 同在 Authenticator 里,但协议/交互不同;TOTP 仍是 6 位码输入式。(Microsoft Learn)
参考链接
- RFC 6238(TOTP)/ RFC 4226(HOTP)标准文本。(IETF Datatracker)
- Google:Authenticator 使用与 2 步验证说明。(Google 帮助)
- Key URI Format(otpauth)。(GitHub)
- Microsoft:Authenticator 作为 OATH TOTP 软件令牌、30 秒周期与二维码生成。(Microsoft Learn)
- OWASP:MFA/Secrets 管理的安全实践。(OWASP Cheat Sheet Series)
附:最小可用接口返回样例(便于对接)
json
// /api/mfa/setup
{
"setupId": "7a4f2b3c-8a7e-4d31-9b5e-2c6a1e...",
"otpauthUri": "otpauth://totp/YourApp:alice@example.com?secret=KZXW6YTB...&issuer=YourApp&period=30&digits=6&algorithm=SHA1",
"secret": "KZXW6YTB..." // 仅在“显示手动输入”时使用
}json
// /api/mfa/verify
{
"ok": true,
"enabledAt": "2025-09-12T08:00:00Z",
"recoveryCodesIssued": 10
}到这一步,你就能用 Google/Microsoft 验证器完成标准 TOTP 绑定与登录校验了。