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:验证邮箱和密码,确认用户身份并检查是否需要MFA
- 阶段2:(可选)验证MFA验证码
- 阶段3:完成登录(处理记住我、创建会话等)
改造登录类:
- 构造函数仅做参数初始化,不执行任何业务逻辑
- 为每个阶段提供独立的方法,比如
validateCredentials()、checkMfaRequired()、validateMfaCode()、completeLogin() - 新增状态属性(比如
$currentStage)来标记当前流程进度,同时存储中间数据(比如已验证的accountId),避免重复查询数据库
处理表单提交:
- 区分两种POST请求:普通登录提交(
$_POST['Login'])和MFA验证码提交($_POST['VerifyMFA']) - 针对不同的请求类型,调用登录类对应的阶段方法
- 区分两种POST请求:普通登录提交(
重构后的代码示例
改造后的登录类
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
相关产品推荐
相关产品推荐

