Hyperf方案 多因素认证(MFA)
最佳选择spomky-labs/otphpRFC 标准活跃维护做 TOTPlbuchs/WebAuthn 做 Passkey。 ┌─────────────────────────────┬───────────────────┬────────────┐ │ 方式 │ 库 │ 场景 │ ├─────────────────────────────┼───────────────────┼────────────┤ ────────────────────── │ TOTP(Google Authenticator)│ spomky-labs/otphp │ 主流 MFA │ ├─────────────────────────────┼───────────────────┼────────────┤ │ WebAuthn/Passkey │ lbuchs/WebAuthn │ 无密码登录 │ ├─────────────────────────────┼───────────────────┼────────────┤ │ 备用码 │ 原生实现 │ 兜底方案 │ └─────────────────────────────┴───────────────────┴────────────┘ --- 安装composerrequire spomky-labs/otphp bacon/bacon-qr-code ---1. TOTP 绑定与验证?php namespace App\Service;use OTPHP\TOTP;use BaconQrCode\Renderer\ImageRenderer;use BaconQrCode\Renderer\Image\SvgImageBackEnd;use BaconQrCode\Renderer\RendererStyle\RendererStyle;use BaconQrCode\Writer;class MfaService{// 生成密钥 二维码 publicfunctionsetup(string$userEmail): array{$totpTOTP::generate();$totp-setLabel($userEmail);$totp-setIssuer(env(APP_NAME,MyApp));$writernew Writer(new ImageRenderer(new RendererStyle(200), new SvgImageBackEnd()));$qrCodebase64_encode($writer-writeString($totp-getProvisioningUri()));return[secret$totp-getSecret(),qr_svg$qrCode,];}// 验证 OTP publicfunctionverify(string$secret, string$code): bool{$totpTOTP::createFromSecret($secret);return$totp-verify($code, null,1);// 允许前后1个时间窗口}// 生成备用码 publicfunctiongenerateBackupCodes(): array{returnarray_map(fn()strtoupper(bin2hex(random_bytes(4))).-.strtoupper(bin2hex(random_bytes(4))), range(1,8));}}---2. MFA 控制器?php namespace App\Controller;use App\Service\MfaService;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\PostMapping;use Hyperf\HttpServer\Annotation\GetMapping;#[Controller(prefix: /api/mfa)]class MfaController{publicfunction__construct(private MfaService$mfa){}// 初始化绑定#[GetMapping(path: setup)]publicfunctionsetup(): array{$userauth()-user();$data$this-mfa-setup($user-email);// 临时存 session确认后再持久化 session([mfa_secret_pending$data[secret]]);return[qr_svg$data[qr_svg]];}// 确认绑定#[PostMapping(path: confirm)]publicfunctionconfirm(): array{$secretsession(mfa_secret_pending);$code$this-request-input(code);if(!$secret||!$this-mfa-verify($secret,$code)){return$this-response-json([errorInvalid code],422);}$backupCodes$this-mfa-generateBackupCodes();auth()-user()-update([mfa_secretencrypt($secret),mfa_enabledtrue,mfa_backup_codesencrypt(json_encode(array_map(password_hash,$backupCodes, array_fill(0,8, PASSWORD_BCRYPT)))),]);return[backup_codes$backupCodes];// 仅展示一次}// 登录时验证#[PostMapping(path: verify)]publicfunctionverify(): array{$userauth()-user();$code$this-request-input(code);$secretdecrypt($user-mfa_secret);if($this-mfa-verify($secret,$code)){session([mfa_passedtrue]);return[verifiedtrue];}// 尝试备用码if($this-verifyBackupCode($user,$code)){session([mfa_passedtrue]);return[verifiedtrue,backup_usedtrue];}return$this-response-json([errorInvalid code],401);}privatefunctionverifyBackupCode($user, string$input): bool{$codesjson_decode(decrypt($user-mfa_backup_codes),true);foreach($codesas$i$hash){if(password_verify(strtoupper($input),$hash)){$codes[$i]USED;// 一次性消费$user-update([mfa_backup_codesencrypt(json_encode($codes))]);returntrue;}}returnfalse;}}---3. MFA 中间件强制二步验证?php namespace App\Middleware;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;class MfaMiddleware implements MiddlewareInterface{publicfunctionprocess(ServerRequestInterface$request, RequestHandlerInterface$handler): ResponseInterface{$userauth()-user();if($user?-mfa_enabled!session(mfa_passed)){return$this-response-json([errorMFA required,redirect/mfa/verify],403);}return$handler-handle($request);}}路由注册 Router::addGroup(/api/admin,function(){Router::get(/dashboard,[DashboardController::class,index]);},[middleware[AuthMiddleware::class, MfaMiddleware::class]]);---4. Migration Schema::table(users,function(Blueprint$table){$table-boolean(mfa_enabled)-default(false);$table-text(mfa_secret)-nullable();$table-text(mfa_backup_codes)-nullable();});--- 核心要点 - 密钥用 encrypt()存储不明文入库 - 备用码用 password_hash 存哈希使用后标记 USED - verify()第三参数1允许30秒时钟偏差 - MFA 状态存 session敏感路由加 MfaMiddleware