mailcow-dockerized 中的 TOTP 二次验证:RobThree TwoFactorAuth 库快速上手指南
【免费下载链接】mailcow-dockerizedmailcow: dockerized - 🐮 + 🐋 = 💕项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized
导读
本文以 mailcow-dockerized 仓库内置的第三方依赖 RobThree TwoFactorAuth 官方文档(getting-started.md)为主体,系统讲解在 PHP 项目中接入基于 TOTP(基于时间的一次性密码)的双因素认证(2FA)的完整流程:从 Composer 安装、创建TwoFactorAuth实例,到生成共享密钥、展示给用户、以及安全地验证动态验证码。同时结合 mailcow 自身的实际接入代码(管理后台 TOTP 注册与登录校验),帮助你理解这一经典库在真实邮件网关项目中的落地方式。读完本文,你将能独立在自己的 PHP 项目中复刻"扫码绑定 + 动态码登录"的完整 2FA 闭环。
1. 安装:通过 Composer 引入依赖
该库的官方推荐安装方式是通过 Composer。在当前 mailcow-dockerized 仓库中,它正是作为 Composer 依赖被引入的,你可以通过 composer.json 与 composer.lock 确认其锁定版本。
如果你在本地项目中使用本地下载的 Composer 可执行文件:
php composer.phar require robthree/twofactorauth如果 Composer 已全局安装:
composer require robthree/twofactorauth安装完成后,库的代码会出现在vendor/robthree/twofactorauth目录下,与本仓库中的 data/web/inc/lib/vendor/robthree/twofactorauth 目录结构一致。
注意:如果你的项目没有使用任何基于 Composer 的框架,需要自行引入 Composer 自动加载器(
vendor/autoload.php),否则无法使用该库。在 mailcow 中,这一步骤由 prerequisites.inc.php 中的require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';完成,之后便可以在全局使用RobThree\Auth\TwoFactorAuth类。
2. 创建实例:new TwoFactorAuth()
引入自动加载后,即可创建用于后续所有操作的实例:
use RobThree\Auth\TwoFactorAuth; $tfa = new TwoFactorAuth();这是最简形式,所有参数都使用默认值。从源码 lib/TwoFactorAuth.php 可以看到,构造函数签名如下:
public function __construct( $issuer = null, // 签发方名称,扫码时展示在用户 App 中 $digits = 6, // 验证码位数,默认 6 位 $period = 30, // 每个验证码的有效秒数,默认 30 秒 $algorithm = 'sha1', // 哈希算法:sha1 / sha256 / sha512 / md5 IQRCodeProvider $qrcodeprovider = null, // QR 码生成器 IRNGProvider $rngprovider = null, // 随机数生成器 ITimeProvider $timeprovider = null // 时间源 )构造函数会对参数做严格校验:$digits与$period必须是正整数,$algorithm必须在受支持的算法列表['sha1', 'sha256', 'sha512', 'md5']内,否则抛出TwoFactorAuthException。这一点在 tests/TwoFactorAuthTest.php 中有对应的异常测试覆盖。
mailcow 的实际实例化方式(见 prerequisites.inc.php):
$qrprovider = new RobThree\Auth\Providers\Qr\BaconQrCodeProvider(); $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);其中$OTP_LABEL在 vars.inc.php 中定义为"mailcow UI",即用户扫码后在其认证器 App 中看到的账户标签;6 位数字、30 秒周期、sha1 算法这三项默认值保证了与 Google Authenticator 等主流 App 的最大兼容性。同时 mailcow 显式注入了BaconQrCodeProvider作为离线 QR 码生成器(原因见第 5 节)。
3. 共享密钥(Shared Secrets):创建与展示
当用户在你的项目中开启双因素(或多因素)认证时,第一步是为该用户创建一个专属密钥:
$secret = $tfa->createSecret();3.1 密钥生成的底层原理
从源码 createSecret 实现 可以看到,createSecret($bits = 80, $requirecryptosecure = true)的工作过程是:
- 根据
$bits(默认 80 位)计算需要的字节数:ceil($bits / 5)——因为 Base32 字母表有 32 个字符,每个字符恰好编码 5 位; - 调用 RNG 提供器生成对应长度的随机字节;
- 若
$requirecryptosecure为true(默认)且当前 RNG 提供器不是加密安全级别,会直接抛出TwoFactorAuthException; - 对每个字节执行
ord($rnd[$i]) & 31掩码取低 5 位,映射到 Base32 字母表ABCDEFGHIJKLMNOPQRSTUVWXYZ234567,最终得到一个大小写统一、无歧义字符的密钥字符串。
默认 80 位对应 16 个 Base32 字符。若需要更高安全性,可传createSecret(160)或更高(推荐 160 及以上,且应为 8 的倍数,参考 RFC 4226 的算法要求),测试 IRNGProviderTest.php 展示了不同$bits下密钥长度的对应关系。
3.2 将密钥告知用户
拿到密钥后,可以用任何你希望的方式展示给用户,例如直接以文本形式让用户手动输入到认证器 App:
<p>请在您的应用中输入以下代码: '<?php echo $secret; ?>'</p>更常见的做法是生成二维码供用户扫码(详见第 5 节),避免手动输入出错。
安全要点(官方文档明确强调):在确认用户能正确使用该密钥之前,应当把密钥保存在当前会话(Session)中,而不是立刻写入用户记录。mailcow 正是这样实现的——在 footer.inc.php 中,
totp_secret由$tfa->createSecret()生成后作为 Twig 全局变量注入页面,而密钥的持久化只发生在用户提交的验证码校验成功之后(见 functions.inc.php 的totp分支)。
4. 验证:verifyCode()
将密钥展示给用户后,最佳实践是立即验证用户的认证器 App 是否已正确录入该密钥并生成了正确的动态码:
$result = $tfa->verifyCode($secret, $_POST['verification']);- 若
$result为true:说明用户已成功将$secret录入其认证器 App,且 App 生成了正确的动态码; - 此时你才可以把
$secret保存到用户记录中,并在用户每次登录时用同一个verifyCode方法校验。
4.1 验证的算法细节
verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0)的完整实现在 lib/TwoFactorAuth.php。其内部逻辑为:
- 用当前时间除以
$period(默认 30 秒)取整,得到当前时间片(timeslice); - 在
-discrepancy到+discrepancy的窗口内逐一计算每个时间片对应的 TOTP 码并比较; - 刻意遍历窗口内所有时间片(即使已匹配成功也继续循环),以保持恒定执行时间,防范计时侧信道攻击;
- 两个验证码的比较使用
hash_equals()(不存在时退化为逐字节异或比较),同样是防时序攻击的常量时间比较(见 codeEquals 实现)。
getCode()则完整实现了 RFC 6238 的 TOTP 计算:将时间戳打包为 8 字节二进制串,用密钥做 HMAC 哈希,取结果末字节低 4 位作为偏移,截取 4 字节并按& 0x7FFFFFFF保留 31 位,最后对10^digits取模并左补零到指定位数(见 getCode 实现)。
4.2 测试向量佐证
测试 TwoFactorAuthTest.php 使用 RFC 4226 附录的官方测试向量验证了算法正确性,例如:
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216)); $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); // 超出时间片同时测试也覆盖了 sha256、sha512 算法下的多个已知时间点输出(见 TwoFactorAuthTest.php),可用于在集成后快速验证你环境中的计算结果是否正确。
4.3 mailcow 中的完整调用链
mailcow 在用户开启 TOTP 时(functions.inc.php)执行:
if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { // 校验通过:将密钥与 key_id 写入 tfa 表并激活 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); $stmt->execute(array($username, $key_id, $_POST['totp_secret'])); unset($_SESSION['pending_tfa_setup']); } else { // 校验失败:提示 totp_verification_failed }而在用户登录时(functions.inc.php),则用持久化在数据库中的$row['secret']与用户提交的$_data['token']再次调用$tfa->verifyCode()。这正是官方文档所述"保存密钥后每次登录都调用同一verifyCode方法"的工程化落地。
5. 补充:把密钥交给用户的最佳实践——二维码
虽然"Getting Started"一章只要求把密钥文本展示给用户,但官方文档明确指出二维码是避免手输错误、并可预填 App 内文本的推荐方式。生成二维码只需:
<p>请使用您的应用扫描下方图片:</p> <img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>">getQRCodeImageAsDataUri($label, $secret, $size = 200)返回 base64 编码的data:URI,二维码内容由 getQRText() 按 Google Authenticator Key URI 格式生成:
otpauth://totp/{label}?secret={secret}&issuer={issuer}&period={period}&algorithm={ALGO}&digits={digits}5.1 默认 QR 提供器与离线替代
重要提醒:库默认使用QRServerProvider,它依赖第三方在线服务生成二维码;如果该服务不可达,用户将看到二维码加载缓慢甚至无法显示。文档建议使用离线提供器,如:
BaconQrCodeProvider(需composer require bacon/bacon-qr-code ^2.0,非 SVG 格式还需 PHP imagick 扩展,可配置$borderWidth、$backgroundColour、$foregroundColour、$format,见 docs/qr-codes/bacon.md);EndroidQrCodeProvider/EndroidQrCodeWithLogoProvider(需endroid/qr-code)。
mailcow 正是选择了BaconQrCodeProvider离线方案(prerequisites.inc.php),并通过 ajax/qr_gen.php 在用户设置页面动态输出二维码:
echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $_GET['token']);在线/离线各提供器的完整列表及自定义提供器(实现IQRCodeProvider接口)的方法,见 docs/qr-codes.md。
6. 继续深入:本仓库中可参考的进阶文档
"Getting Started"只是该库文档体系的第一篇。本仓库还随库携带了以下进阶文档,均可直接阅读:
- optional-configuration.md:构造函数全部可选参数(
$issuer、$digits、$period、$algorithm、$qrcodeprovider、$rngprovider、$timeprovider)的默认值与用途;RNG 提供器的自动选择顺序(PHP7+ 的CSRNGProvider→MCryptRNGProvider→OpenSSLRNGProvider→ 非加密安全的HashRNGProvider,见 getRngProvider 实现);以及用ensureCorrectTime()借助NTPTimeProvider/HttpTimeProvider校验服务器时间偏差(默认容忍 5 秒); - improved-code-verification.md:
verifyCode()的$discrepancy(默认 1,表示在当前时间片前后各检查几个时间片)、$time(指定时间点,多用于单元测试)与$timeslice(按引用返回匹配的时间片)的进阶用法——将上次成功匹配的 timeslice 与密钥一同存储,可有效防御重放攻击; - index.md:文档目录总览;
- demo/demo.php:一个无需框架、自带
spl_autoload_register的最小可运行示例,完整演示了"创建密钥 → 展示二维码 → 计算当前码 → 校验码 → 检查服务器时间"的全过程,适合作为快速原型参考。
7. 常见问题与注意事项小结
- 服务器时间必须准确:TOTP 依赖时间片对齐。官方演示脚本(demo/demo.php)专门提示"确保服务器时间已 NTP 同步",并演示了用
ensureCorrectTime()检测偏差。若服务器时钟漂移过大,用户会持续验证失败; - 密钥在验证成功前只放 Session:避免把无效密钥污染用户数据(mailcow 的
pending_tfa_setup会话标记即为此设计); - 不要无限调大
$discrepancy:时间片检查窗口越大,验证码有效时间越长,被恶意重用的风险越高; - 默认参数 = 最大兼容性:
digits=6、period=30、algorithm=sha1与 Google Authenticator 等主流 App 完全兼容;一旦改动,需要用户使用支持对应参数组合的特定 App。
至此,从安装、实例化、密钥创建与展示、到安全校验的完整 TOTP 2FA 接入流程已经讲清,并且每一步都能在 mailcow-dockerized 仓库的源码与测试中找到真实对应实现,可作为你在其他 PHP 项目中接入双因素认证的可靠参考。
【免费下载链接】mailcow-dockerizedmailcow: dockerized - 🐮 + 🐋 = 💕项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考