高安全性 PHP 2FA 开发指南:Authenticator 扫码验证实现方案

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扫描)。
bash 复制代码
composer require robthree/twofactorauth
sudo apt-get install php-imagick
composer require bacon/bacon-qr-code:^2.0 

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

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

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

php 复制代码
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码后,输入Authenticator生成的动态码,验证通过则意味着密钥交换正确,此时可启用two_factor_enabled字段。

前端模板

html 复制代码
<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,则完成业务流程(启用二次认证)。

php 复制代码
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应用可能不允许截图):

相关推荐
叫我阿柒啊1 天前
从Java全栈到前端框架:一场真实的技术面试对话
java·vue.js·spring boot·微服务·typescript·前端开发·后端开发
Java爱好狂.12 天前
Java面试Redis核心知识点整理!
java·数据库·redis·分布式锁·java面试·后端开发·java八股文
Javatutouhouduan13 天前
SpringBoot整合reids之JSON序列化文件夹操作
java·spring boot·spring·bootstrap·html·后端开发·java架构师
aiguangyuan15 天前
Node项目中两个常用的环境变量配置工具
node·后端开发
Java爱好狂.17 天前
复杂知识简单学!Springboot加载配置文件源码分析
java·spring boot·后端·spring·java面试·后端开发·java程序员
realhuizhu17 天前
拿着顶级服务器跑慢查询,就像开着法拉利送外卖
ai编程·sql优化·后端开发·数据库性能·deepseek
Javatutouhouduan19 天前
Java面试常问Redis核心知识点整理!
java·数据库·redis·java面试·后端开发·java架构师·java程序员
realhuizhu19 天前
你的接口很好,但在使用者眼里,它可能只是个打不开的黑盒
效率工具·后端开发·api文档·开发者体验·promptengineering