多因素认证(Multi‑Factor Authentication, MFA)是给账号加第二把锁的通用做法。在 Web 里最常见的是“密码 + 一次性动态验证码(TOTP)”。本文从原理到实现、从后端到前端、从安全到体验,手把手带你把 TOTP‑MFA 上到你的站点。
什么是 TOTP
- TOTP(Time‑based One‑Time Password)基于共享密钥和时间窗口(通常 30 秒)生成 6 位一次性验证码。
- 开放标准:RFC 6238(在 HOTP/RFC 4226 基础上加入时间维度)。
- 常见验证器:Google Authenticator、Microsoft Authenticator、1Password、Authy 等。
工作原理简述:
- 服务器与用户设备共享一个“密钥”(Secret)。
- 每 30 秒,设备用 Secret + 时间片计算出新的 6 位数字;服务器用同样算法校验。
- 为兼容网络/设备时间抖动,通常允许 ±1 个时间窗口(即 90 秒内有效)。
端到端流程(绑定与登录)
1) 绑定(首次或重置时)
- 用户已通过“已知身份”校验(如已输入正确用户名+密码)。
- 服务器为该用户生成(或派生)一个 Secret。
- 服务器构造 otpauth URI,并返回一个二维码给前端。
- 用户用手机验证器“扫描二维码”或手动输入 Secret 完成绑定。
2) 登录
- 用户输入用户名 + 密码,或切换为“代码登录”(仅输入 6 位 TOTP)。
- 服务器基于用户的 Secret 计算当前时间片可接受的 TOTP,校验通过则登录成功。
服务器端实现要点(以 PHP 为例)
1. 生成/派生 Secret
两种常见方式:
- 随机生成(推荐在有安全存储时)
- 从用户稳定材料派生(无持久化时的折中)
随机生成示例(20 字节随机后做 Base32):
function base32EncodeNoPad(string $data): string { $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $bits = ''; for ($i = 0; $i < strlen($data); $i++) { $bits .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT); } $chunks = str_split($bits, 5); $out = ''; foreach ($chunks as $c) { $c = str_pad($c, 5, '0', STR_PAD_RIGHT); $out .= $alphabet[bindec($c)]; } return rtrim($out, '='); } function generateTotpSecret(int $bytes = 20): string { return base32EncodeNoPad(random_bytes($bytes)); }
无持久化时的安全派生(示例):
- 使用 HMAC(pepper, lowercase(username) + ':' + passwordHashInDB) 生成 20 字节,再做 Base32。
- 密码变更会导致 Secret 随之变更,安全性优于“仅用户名派生”。
function deriveSecretFromPasswordHash(string $username, string $passwordHash, string $pepper): string { $normalized = strtolower(trim($username)); $raw = hash_hmac('sha1', $normalized . ':' . $passwordHash, $pepper, true); // 20 bytes return base32EncodeNoPad($raw); }
2. 生成 otpauth URI
$issuer = '你的站点名'; $account = $username; // 建议为登录名或邮箱 $query = http_build_query([ 'secret' => $secret, 'issuer' => $issuer, 'algorithm' => 'SHA1', 'digits' => 6, 'period' => 30, ]); $otpauth = "otpauth://totp/" . rawurlencode("$issuer:$account") . "?$query";
3. 生成二维码(云服务或本地库)
- 云服务(不落地文件):推荐 https://api.qrtool.cn/
参考参数说明,见文档链接:qrtool.cn 开放参数
$qrUrl = 'https://api.qrtool.cn/?' . http_build_query([ 'text' => $otpauth, 'size' => 240, // 宽高 'margin' => 2, // 白边 'level' => 'M', // 容错级别:L/M/Q/H ]);
- 或使用本地 QR 生成库(如 PHP 扩展/JS 前端渲染),减少对外网依赖。
4. 验证 TOTP
function computeTotp(string $secretBytes, int $time, int $period = 30): string { $counter = intdiv($time, $period); $msg = pack('N*', 0) . pack('N*', $counter); // 8 字节 $hmac = hash_hmac('sha1', $msg, $secretBytes, true); $offset = ord($hmac[19]) & 0x0F; $bin = substr($hmac, $offset, 4); $val = unpack('N', $bin)[1] & 0x7FFFFFFF; return str_pad((string)($val % 1000000), 6, '0', STR_PAD_LEFT); } function verifyTotp(string $account, string $code, int $window = 1, int $period = 30): bool { if (!preg_match('/^\d{6}$/', $code)) return false; // 拿到 secretBytes(从存储取,或按约定规则派生) $secretBytes = ...; // decode Base32 or from HMAC派生的原始字节 $now = time(); for ($w = -$window; $w <= $window; $w++) { if (hash_equals(computeTotp($secretBytes, $now + $w * $period, $period), $code)) { return true; } } return false; }
前端实现要点
绑定模态的两步体验
- 步骤 1:输入“账号 + 密码”,点击“生成二维码”。
- 步骤 2:显示二维码与 Secret,提示“仅需扫码一次,后续输入验证码即可”。
- 二维码支持点击刷新(追加时间戳防缓存)。
- 可在 记住最近一次生成的二维码(建议设置有效期,如 7 天),下次打开直接展示,点“删除”再清空。
localStorage
登录表单的模式切换
- 提供“密码登录 / 代码登录”切换(PC 端)。
- “代码登录”仅输入 6 位 TOTP;支持记忆用户上一次选择的登录方式(localStorage)。
- 手机端可只保留密码登录入口(按业务需要)。
安全与合规建议
- 必要的身份校验:生成二维码前务必确保用户已通过“已知身份”校验(如用户名+密码正确)。
- 传输安全:全站 HTTPS。
- 限流与防枚举:接口级别加速率限制(基于 IP/用户名/设备指纹)。
- 时间同步:TOTP 依赖时间;提示用户开启自动对时。服务器端也要对时。
- 存储安全:
- 若能持久化,Secret 应加密存储或 KMS 托管;备份码/恢复流程要完善。
- 无持久化时,优先选择“与密码哈希绑定的派生方案”,降低被离线推导风险。
- 设备丢失/换机:提供重置 MFA 的客服/自助流程(邮箱验证、短信、人工审核等)。
- 风险场景二审:高风险操作(如提现、修改安全项等),可以强制追加 TOTP 二次确认。
- 审计与告警:记录绑定、解绑、失败尝试等;异常频率触发告警。
体验优化建议
- 让“扫描二维码”与“手动输入密钥”两种方式并存。
- 给出明确文案:
- “仅需扫码一次,后续登录输入验证码即可”
- “验证码每 30 秒更新”
- “验证码无效请检查手机时间是否自动同步”
- 错误提示用轻量 Toast,不要阻断表单输入。
- 对桌面端记忆上次登录方式;对移动端可隐藏 MFA(按业务决定)。
常见问题(FAQ)
- Q:换手机后无法登录?
A:提供“重置 MFA”的路径(邮箱验证/人工审核);建议用户换机前在新设备再次绑定。 - Q:验证码总是错?
A:检查手机时间同步;检查是否输入的是当前滚动周期的 6 位码;服务器端可允许 ±1 时间窗口。 - Q:二维码服务无法访问?
A:可切换到备用 QR 服务或使用本地 QR 库;推荐 https://api.qrtool.cn/,参数 text/size/margin/level 见其开放文档。
参考链接
- RFC 6238(TOTP)与 RFC 4226(HOTP)
- QR 生成服务文档:qrtool.cn 开放参数
- API 入口:https://api.qrtool.cn/
总结
TOTP‑MFA 在 Web 的落地不复杂,关键是“何时生成/如何保存/怎样验证/如何兜底”。遵循上面的绑定‑登录两阶段流程、加上合适的安全与体验策略,你就能为站点带来显著的安全提升,同时保持良好的使用体验。