You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP登录流程集成MFA验证的最优实现方案咨询

问题描述

我有一个PHP登录类,采用链式调用执行一系列函数,部分函数可根据条件跳过。登录流程要求用户先在表单输入邮箱和密码,当登录表单验证通过后,如果用户的浏览器/设备未在SQL数据库中记录,需要提示用户输入Multi-Factor Authentication(MFA)验证码。

目前我通过单次登录表单提交执行整个类,但遇到了一个挑战:如何高效重新展示MFA输入表单?想咨询最有效的实现方式:是单独创建MFA提示函数,还是可以将所有流程整合到单个类中?

现有代码

登录类代码

class LoginProcess {
private $errors = array();
private $notices = array();
private $accountId;
private $skipMFA = false;
private $skipRememberMe;

public function __construct($email, $password, $rememberMe) {
    $this->email = $email;
    $this->password = $password;
    $this->rememberMe = $rememberMe;

    $this->authInitiate($email, $password, $rememberMe); 
    if (empty($this->errors)) {
        $this->initiateMFA();
    }
    if (empty($this->errors) && !$this->skipMFA) {
        $this->generateMFA();
        $this->promptMFA();
    }
    if (empty($this->errors) && !$this->skipRememberMe) {
        $this->rememberMe();
    }
    if (empty($this->errors)) {
        $this->authUser();
    }       
}

表单处理代码

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['Login'])) {
        $input = [
        'email' => $_POST['email'] ?? '',
        'password' => $_POST['password'] ?? '',
        'remember_me' => isset($_POST['remember_me']) ? true : false
    ];
    $loginProcess = new LoginProcess($input['email'], $input['password'], $input['remember_me']);
    $errors = $loginProcess->getErrors();
    $notices = $loginProcess->getNotices();
    $report = array('errors' => $errors, 'notices' => $notices);
    foreach ($report['errors'] as $error) {
        echo $error . '<br>';
    }
    foreach ($report['notices'] as $notice) {
        echo $notice . '<br>';
    }
}
}
解决方案

推荐将所有流程整合到单个类中,但需要重构现有代码的执行逻辑——不要在构造函数里一次性跑完所有流程,而是把登录拆分成多个可分步执行的阶段,用状态来控制当前处于登录流程的哪一步。这样既能保持代码的内聚性,又能灵活处理MFA的二次提交需求。

具体重构思路

  1. 拆分流程阶段:把登录流程拆分为三个核心阶段:

    • 阶段1:验证邮箱和密码,确认用户身份并检查是否需要MFA
    • 阶段2:(可选)验证MFA验证码
    • 阶段3:完成登录(处理记住我、创建会话等)
  2. 改造登录类:

    • 构造函数仅做参数初始化,不执行任何业务逻辑
    • 为每个阶段提供独立的方法,比如validateCredentials()、checkMfaRequired()、validateMfaCode()、completeLogin()
    • 新增状态属性(比如$currentStage)来标记当前流程进度,同时存储中间数据(比如已验证的accountId),避免重复查询数据库
  3. 处理表单提交:

    • 区分两种POST请求:普通登录提交($_POST['Login'])和MFA验证码提交($_POST['VerifyMFA'])
    • 针对不同的请求类型,调用登录类对应的阶段方法

重构后的代码示例

改造后的登录类

class LoginProcess {
    private $errors = [];
    private $notices = [];
    private $email;
    private $password;
    private $rememberMe;
    private $accountId;
    private $mfaRequired = false;
    private $currentStage = 'credentials'; // 可选值: credentials, mfa, complete

    public function __construct($email, $password, $rememberMe) {
        $this->email = $email;
        $this->password = $password;
        $this->rememberMe = $rememberMe;
    }

    // 阶段1:验证邮箱密码,检查是否需要MFA
    public function validateCredentials() {
        if ($this->currentStage !== 'credentials') return $this;

        // 执行原authInitiate逻辑,验证账号密码
        $this->authInitiate($this->email, $this->password);
        if (!empty($this->errors)) return $this;

        // 检查是否需要MFA(原initiateMFA逻辑)
        $this->mfaRequired = $this->checkIfDeviceUnknown();
        if ($this->mfaRequired) {
            $this->currentStage = 'mfa';
            $this->generateMfaCode(); // 生成并发送MFA验证码
            $this->notices[] = '请输入MFA验证码完成登录';
        } else {
            $this->currentStage = 'complete';
        }
        return $this;
    }

    // 阶段2:验证MFA验证码
    public function validateMfaCode($mfaCode) {
        if ($this->currentStage !== 'mfa') {
            $this->errors[] = '请先完成账号密码验证';
            return $this;
        }

        // 验证MFA逻辑
        if (!$this->verifyMfaCode($this->accountId, $mfaCode)) {
            $this->errors[] = 'MFA验证码无效';
            return $this;
        }

        $this->currentStage = 'complete';
        return $this;
    }

    // 阶段3:完成登录
    public function completeLogin() {
        if ($this->currentStage !== 'complete') return $this;

        // 处理记住我逻辑
        if ($this->rememberMe) {
            $this->setRememberMeCookie($this->accountId);
        }

        // 创建用户会话
        $this->authUser();
        return $this;
    }

    // 辅助方法(原有的业务逻辑保留)
    private function authInitiate($email, $password) {
        // 原验证邮箱密码逻辑,设置$this->accountId和$this->errors
    }

    private function checkIfDeviceUnknown() {
        // 检查当前设备是否在数据库中,返回bool
    }

    private function generateMfaCode() {
        // 生成并发送MFA验证码逻辑
    }

    private function verifyMfaCode($accountId, $code) {
        // 验证MFA验证码逻辑
    }

    private function setRememberMeCookie($accountId) {
        // 记住我逻辑
    }

    private function authUser() {
        // 创建用户会话逻辑
    }

    // 获取错误和通知
    public function getErrors() {
        return $this->errors;
    }

    public function getNotices() {
        return $this->notices;
    }

    public function getCurrentStage() {
        return $this->currentStage;
    }

    public function getAccountId() {
        return $this->accountId;
    }
}

改造后的表单处理代码

session_start(); // 开启会话存储中间状态

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 处理普通登录提交
    if (isset($_POST['Login'])) {
        $input = [
            'email' => $_POST['email'] ?? '',
            'password' => $_POST['password'] ?? '',
            'remember_me' => isset($_POST['remember_me'])
        ];

        $login = new LoginProcess($input['email'], $input['password'], $input['remember_me']);
        $login->validateCredentials();

        if ($login->getCurrentStage() === 'mfa') {
            // 存储登录对象到会话,保留中间状态
            $_SESSION['login_process'] = serialize($login);
            // 展示MFA输入表单
            echo '<form method="POST">
                    <input type="hidden" name="account_id" value="' . $login->getAccountId() . '">
                    <input type="text" name="mfa_code" placeholder="请输入MFA验证码">
                    <button type="submit" name="VerifyMFA">验证MFA</button>
                  </form>';
        } elseif ($login->getCurrentStage() === 'complete') {
            $login->completeLogin();
            // 登录成功,跳转首页
            header('Location: /dashboard');
            exit;
        } else {
            // 展示错误
            foreach ($login->getErrors() as $error) {
                echo $error . '<br>';
            }
            // 重新展示登录表单
            include 'login_form.php';
        }
    }

    // 处理MFA验证码提交
    if (isset($_POST['VerifyMFA'])) {
        if (!isset($_SESSION['login_process'])) {
            echo '登录会话已过期,请重新登录';
            include 'login_form.php';
            exit;
        }

        $login = unserialize($_SESSION['login_process']);
        $mfaCode = $_POST['mfa_code'] ?? '';
        $login->validateMfaCode($mfaCode);

        if ($login->getCurrentStage() === 'complete') {
            $login->completeLogin();
            unset($_SESSION['login_process']); // 清理会话
            header('Location: /dashboard');
            exit;
        } else {
            // MFA验证失败,重新展示MFA表单
            foreach ($login->getErrors() as $error) {
                echo $error . '<br>';
            }
            echo '<form method="POST">
                    <input type="hidden" name="account_id" value="' . $login->getAccountId() . '">
                    <input type="text" name="mfa_code" placeholder="请输入MFA验证码">
                    <button type="submit" name="VerifyMFA">验证MFA</button>
                  </form>';
        }
    }
} else {
    // GET请求,展示登录表单
    include 'login_form.php';
}

为什么不推荐单独创建MFA提示函数?

  • 单独拆分MFA函数会导致登录逻辑分散,增加维护成本
  • 需要额外处理跨请求的状态传递(比如已验证的账号ID),反而不如在单个类中统一管理更清晰
  • 链式调用的设计本身适合分步处理,改造后依然可以保持类的内聚性

内容的提问来源于stack exchange,提问作者Adnan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:46:08