Skip to content

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、Python pyotp、Java java-otp)生成 secret、构造 otpauth://、校验验证码并完成持久化与风控。(Otplib)

一、常见验证器与差异

  • Google Authenticator:标准 TOTP,支持扫描 otpauth://,可选账号云同步(注意隐私权衡)。(Google 帮助)
  • Microsoft Authenticator:同样支持 OATH TOTP(也有推送批准与“数字匹配”功能,和 TOTP 是两种形态)。(Microsoft Learn)

这两款在“扫描二维码 → 本地计算一次性验证码”的 TOTP 场景是互通的。


二、工作原理(够用但准确)

  1. HOTP(事件计数)code = Truncate(HMAC(K, counter)),每用一次计数+1。(IETF)
  2. TOTP(时间计数):把计数器换成时间片 C = floor((now - T0)/period),常见 period=30s,默认 6 位数字。支持 SHA1/256/512。(IETF Datatracker)
  3. 密钥分发:用 otpauth://TYPE/Label?secret=...&issuer=...&period=30&digits=6&algorithm=SHA1secretBase32 编码。验证器扫二维码后就能开始计算。(GitHub)

三、你描述的“绑定流程”有个小修正

“我把码和密钥发给后端完成绑定” —— 不建议这样做。

推荐安全流程(后端早已生成并持有 secret):

  1. 后端生成 secret(Base32),构造 otpauth://,返回 二维码(或明文 secret 供手输)
  2. 用户用验证器扫描 → 验证器本地算出 6 位码。
  3. 用户只回填一次性验证码(6 位码)到后端;
  4. 后端用已保存的 secret 验证这 6 位码(允许 ±1 时间片容差),通过则标记已绑定。
  5. 保存与保护 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 secretissuerdigitsperiod 等参数定义与默认值。(GitHub)
  • Google 文档:Authenticator 的 2 步验证与离线生成机制;近年新增“可选云同步”。(Google 帮助)
  • Microsoft 文档:Authenticator 作为软件 OATH TOTP 令牌的说明;30s 周期;ASP.NET Core 生成二维码指南。(Microsoft Learn)
  • 实现库:Node.js otplib、Python pyotp、Java java-otp。(npm)
  • OWASP:MFA 与机密管理实践。(OWASP Cheat Sheet Series)

十一、最简落地清单(按顺序做就能跑)

  1. 后端:选库(otplib/pyotp/java-otp),实现 createMfaSetup() + verifyMfaCode();密钥加密入库。(npm)
  2. 前端:拿 otpauthUriqrcode 渲染二维码,提供“我已扫码,输入 6 位验证码”表单。
  3. 接口/mfa/setup(出二维码)→/mfa/verify(校验码,窗口 ±1);失败次数上限与冷却。
  4. 登录:若用户已开启 2FA,密码通过后进入 /mfa/challenge 流程再校验一次码。
  5. 运维: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)


参考链接


附:最小可用接口返回样例(便于对接)

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 绑定与登录校验了。

本站总访问