• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
28.7的博客
等小白学会了游泳,我们天天海边,学会了滑板,我们夏天去冲浪冬天去滑雪,只要你愿意,我们去经历各种各样有趣的事情~ “鸡毛!我学会了发现冰激凌烧烤奶茶火锅,我们天天去吃!现在就去!” “●﹏●;”
博客园    首页    新随笔    联系   管理    订阅  订阅
高安全性 PHP 2FA 开发指南:Authenticator 扫码验证实现方案
高安全性 PHP 2FA 开发指南:Authenticator 扫码验证实现方案 本文详解 PHP 环境下基于 TOTP 协议的双因素认证(2FA)实现方案,核心依赖 robthree/twofactorauth 与 bacon/bacon-qr-code 扩展,需配合 php-imagick 组件。实现流程清晰:用户登录后,系统生成唯一密钥并绑定账号,通过 Bacon QR Code 生成二维码,供 Google Authenticator 等 App 扫描绑定;用户输入 App 生成的 6 位动态码,服务器通过 TwoFactorAuth 验证有效性后,激活账号二次认证。方案支持 6 位验证码、30 秒有效期配置,采用 SHA1 加密,密钥与用户账号强关联,初始状态禁用,验证通过后启用,既保障账户安全,又通过自主选择机制平衡操作便捷性,提供了完整的代码示例与测试流程。

PHP 实现双因素身份认证(2FA)

什么是双因素

双因素身份认证(英文简称 2FA),是一种在传统 “账号 + 密码”(单因素认证)基础上增加的第二层安全验证机制,核心逻辑是要求用户同时提供两种不同类型的 “身份凭证” 才能完成登录,通过 “多重验证” 阻挡非法访问,大幅提升账户安全性。

  1. 知识因素:用户 “知道” 的信息(如账号密码、安全问题答案);
  2. 持有因素:用户 “拥有” 的物品 / 设备(如手机、U 盾、动态令牌);
  3. 生物因素:用户 “本身” 的生物特征(如指纹、人脸、声纹)。

双因素认证的本质的是:将 “知识因素”(必选的账号密码)与另外两类因素中的任意一种组合,形成 “密码 + X” 的验证逻辑(X 为持有因素或生物因素)

传统 “账号 + 密码” 容易因密码泄露(如撞库、钓鱼、密码被盗)导致账户被盗,而双因素认证中,即使第一层密码被破解,第二层凭证(如动态码、U 盾)仍能形成安全屏障 —— 非法入侵者无法获取用户的物理设备或生物特征,也就无法完成登录。
比如你之前开发的系统中,用户需先输入正确的账号密码,再输入手机 APP 生成的动态码,才算登录成功,即使密码泄露,没有手机上的实时动态码,攻击者也无法登录账户

认证流程

双因素身份认证,简单理解就是使用账户密码登录后需要使用一个动态码确认,账户密码加动态码两种方式登录,多一步就多一点安全性,但这种方式也牺牲了一定方便性。因此,有多种形式的动态码确认,常见的有:

  • 基于TOTP验证APP,例如Google Authenticator、微软的Authenticator;
  • 网上银行的U盾,这类第三方专属物理设备验证;
  • 邮箱、人脸等。

基于安全性,此类二次验证可由用户自行选择(比如部分用户更喜欢邮箱验证码),但基于TOTP验证APP的方法安全性相对最高。

安装相应扩展

在创建二次认证前,需通过业务为用户绑定唯一标识(用于定位用户、生成用户画像),并让服务器生成唯一密钥(Secret)与用户账号绑定。当用户通过手机App(如Google Authenticator)扫描密钥生成的二维码后,密钥会存储在本地。每次登录时,App基于密钥和当前时间生成6位动态验证码,服务器验证该验证码有效性。

需安装的扩展:

  • robthree/twofactorauth:创建绑定用户密钥,兼容主流Authenticator应用;
  • bacon/bacon-qr-code:生成二维码(供手机App扫描)。
composer require robthree/twofactorauth
sudo apt-get install php-imagick
composer require bacon/bacon-qr-code:^2.0 

2FA相关扩展安装示意

环节一:生成密钥与展示二维码

当用户登录后(通过Cookie或Session绑定用户),主动进入“启用二次认证”页面时,系统需生成密钥并展示二维码,供用户用手机App扫描。

以下代码为起点,用于标识用户、创建QR码供绑定:

public function setup(): View|Redirect
{
    // 先标识用户
    $userId = Session::get('userID');
    $user = TpUserData::find($userId);
    if (!$user) {
        return redirect('/ViewController/login.shtml')->with('error', '请先登录');
    }

    // 创建QR码
    $qrCodeProvider = new BaconQrCodeProvider(
        4,                // 二维码大小
        '#ffffff',         // 背景色
        '#000000',         // 前景色
        'svg'              // 输出格式
    );
	
    // 绑定QR标识
    $tfa = new TwoFactorAuth(
        $qrCodeProvider,
        '28.7Blog'  // 应用名称
    );

    $tfa = new TwoFactorAuth(
        $qrCodeProvider,
        '28.7Blog',
        6,              // 验证码长度
        30,             // 验证码有效期(秒)
        \RobThree\Auth\Algorithm::Sha1  // 加密算法
    );

    if (empty($user->two_factor_secret)) {
        $secret = $tfa->createSecret();
        $user->two_factor_secret = $secret;
        $user->two_factor_enabled = 0;  // 初始禁用二次认证
        $user->save();
    } else {
        $secret = $user->two_factor_secret;
    }

    // 以用户邮箱做标识,创建密钥,绑定QR
    $account = $user->email ?? $user->username; 
    $qrCodeUri = $tfa->getQRCodeImageAsDataUri($account, $secret);

    return view('demo/auth', [
        'qrCodeUri' => $qrCodeUri,
        'secret' => $secret,
        'userId' => $userId,
    ]);
}

此时密钥初始为空,需用户后续操作激活。

密钥初始状态示意

在模板中渲染QR码后,此时QR码已生成但未生效(手机端未绑定,且two_factor_enabled字段未开启),因此每次刷新页面密钥会跟随刷新,直到业务流程完成。

QR码渲染示意

验证验证码并启用二次认证

当用户扫描QR码后,输入Authenticator生成的动态码,验证通过则意味着密钥交换正确,此时可启用two_factor_enabled字段。

前端模板

<img src="{$qrCodeUri}" alt="扫描二维码添加到Authenticator">

<form action="/TwoFactorController/verify" method="post">
    <input type="text" name="code" placeholder="请输入Authenticator中的6位验证码" required>
    <button type="submit">验证并启用</button>
</form>

控制器验证方法

当用户输入扫描QR码后创建的验证码时,通过verify()方法校验。若$tfa->verifyCode()返回true,则完成业务流程(启用二次认证)。

public function center()
{
    echo "Success";
}

public function verify(): Redirect
{
    if (!Request::isPost()) {
        return redirect('/TwoFactorController/setup')->with('error', '非法请求');
    }

    $userId = Session::get('userID');
    $user = TpUserData::find($userId);
    if (!$user || empty($user->two_factor_secret)) {
        return redirect('/TwoFactorController/setup')->with('error', '请先初始化二次认证');
    }

    $code = input('post.code', '');
    if (strlen($code) !== 6 || !is_numeric($code)) {
        return back()->with('error', '验证码格式错误(需6位数字)');
    }

    $qrCodeProvider = new BaconQrCodeProvider(
        4,                
        '#ffffff',         
        '#000000',         
        'svg'             
    );
    $tfa = new TwoFactorAuth(
        $qrCodeProvider,
        '28.7Blog'
    );

    $isValid = $tfa->verifyCode($user->two_factor_secret, $code, 2);

    if ($isValid) {
        $user->two_factor_enabled = 1;
        $user->save();
        return redirect('/TwoFactorController/center')->with('success', '二次认证已启用');
    } else {
        return back()->with('error', '验证码无效或已过期(请检查时间同步)');
    }
}

测试流程

使用手机扫描生成的二维码(由于手机策略,部分Authenticator应用可能不允许截图):

QR码扫描示意

手机端Authenticator绑定示意

验证成功示意

posted on 2025-11-16 22:31  28的博客  阅读(66)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3

摘自:28.7的博客

蜀ICP备2025124055号-2