本文目录导读:

我来为您详细介绍如何在PHP项目中实现TOTP(基于时间的一次性密码)与扫码绑定功能。
TOTP 基本原理
TOTP基于HMAC-SHA1算法,使用时间和共享密钥生成一次性密码。
所需库安装
使用Composer安装相关库:
composer require spomky-labs/otphp composer require endroid/qr-code
或者使用更轻量的库:
composer require pragmarx/google2fa composer require simplesoftwareio/simple-qrcode
后端PHP实现
1 TOTP服务类
<?php
// app/Services/TotpService.php
namespace App\Services;
use OTPHP\TOTP;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
class TotpService
{
private $issuer;
private $digits;
private $period;
public function __construct()
{
$this->issuer = 'YourAppName'; // 应用名称
$this->digits = 6; // 验证码位数
$this->period = 30; // 有效期(秒)
}
/**
* 生成TOTP密钥和二维码数据
*/
public function generateSecret(string $username): array
{
// 创建TOTP对象
$totp = TOTP::create();
$totp->setLabel($username);
$totp->setIssuer($this->issuer);
$totp->setDigits($this->digits);
$totp->setPeriod($this->period);
// 生成密钥
$secret = $totp->getSecret();
// 生成OTP Auth URI(用于二维码)
$otpAuthUri = $totp->getProvisioningUri();
return [
'secret' => $secret,
'uri' => $otpAuthUri,
'qr_code' => $this->generateQrCode($otpAuthUri)
];
}
/**
* 生成二维码
*/
private function generateQrCode(string $data): string
{
$qrCode = QrCode::create($data)
->setEncoding(new Encoding('UTF-8'))
->setErrorCorrectionLevel(ErrorCorrectionLevel::Medium)
->setSize(300)
->setMargin(10);
$writer = new PngWriter();
$result = $writer->write($qrCode);
// 返回Base64编码的图片
return base64_encode($result->getString());
}
/**
* 验证TOTP码
*/
public function verifyTotp(string $secret, string $code): bool
{
try {
$totp = TOTP::createFromSecret($secret);
$totp->setDigits($this->digits);
$totp->setPeriod($this->period);
return $totp->verify($code);
} catch (\Exception $e) {
return false;
}
}
/**
* 获取当前有效的TOTP码(用于测试)
*/
public function getCurrentCode(string $secret): string
{
$totp = TOTP::createFromSecret($secret);
$totp->setDigits($this->digits);
$totp->setPeriod($this->period);
return $totp->now();
}
}
2 用户控制器
<?php
// app/Http/Controllers/TotpController.php
namespace App\Http\Controllers;
use App\Services\TotpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class TotpController extends Controller
{
protected $totpService;
public function __construct(TotpService $totpService)
{
$this->totpService = $totpService;
}
/**
* 显示开启TOTP页面
*/
public function showSetupForm()
{
return view('totp.setup');
}
/**
* 获取TOTP配置信息
*/
public function getSetupInfo(Request $request)
{
$user = Auth::user();
// 生成TOTP密钥和二维码
$totpData = $this->totpService->generateSecret($user->email);
// 临时存储密钥,待验证后正式保存
Cache::put('totp_secret_' . $user->id, $totpData['secret'], now()->addMinutes(10));
return response()->json([
'secret' => $totpData['secret'],
'qr_code' => 'data:image/png;base64,' . $totpData['qr_code'],
'uri' => $totpData['uri']
]);
}
/**
* 验证并开启TOTP
*/
public function enableTotp(Request $request)
{
$request->validate([
'code' => 'required|string|size:6'
]);
$user = Auth::user();
$tempSecret = Cache::get('totp_secret_' . $user->id);
if (!$tempSecret) {
return response()->json([
'success' => false,
'message' => '密钥已过期,请重新获取'
], 400);
}
// 验证TOTP码
if (!$this->totpService->verifyTotp($tempSecret, $request->code)) {
return response()->json([
'success' => false,
'message' => '验证码错误'
], 400);
}
// 保存TOTP密钥到数据库
$user->totp_secret = $tempSecret;
$user->totp_enabled = true;
$user->save();
// 清除临时密钥
Cache::forget('totp_secret_' . $user->id);
// 记录恢复码
$recoveryCodes = $this->generateRecoveryCodes();
$user->recovery_codes = json_encode($recoveryCodes);
$user->save();
return response()->json([
'success' => true,
'message' => 'TOTP验证已开启',
'recovery_codes' => $recoveryCodes
]);
}
/**
* 验证TOTP登录
*/
public function verifyTotpLogin(Request $request)
{
$request->validate([
'code' => 'required|string|size:6',
'user_id' => 'required|integer'
]);
$user = User::find($request->user_id);
if (!$user || !$user->totp_enabled) {
return response()->json([
'success' => false,
'message' => '用户未开启TOTP验证'
], 400);
}
// 先检查是否是恢复码
if ($this->isRecoveryCode($user, $request->code)) {
// 处理恢复码逻辑
$this->consumeRecoveryCode($user, $request->code);
return response()->json(['success' => true]);
}
// 验证TOTP码
if ($this->totpService->verifyTotp($user->totp_secret, $request->code)) {
return response()->json(['success' => true]);
}
return response()->json([
'success' => false,
'message' => '验证码错误'
], 400);
}
/**
* 生成恢复码
*/
private function generateRecoveryCodes(): array
{
$codes = [];
for ($i = 0; $i < 8; $i++) {
$codes[] = [
'code' => strtoupper(bin2hex(random_bytes(4))),
'used' => false
];
}
return $codes;
}
/**
* 检查是否是恢复码
*/
private function isRecoveryCode($user, string $code): bool
{
$recoveryCodes = json_decode($user->recovery_codes, true);
foreach ($recoveryCodes as $recoveryCode) {
if (!$recoveryCode['used'] && $recoveryCode['code'] === strtoupper($code)) {
return true;
}
}
return false;
}
/**
* 使用恢复码
*/
private function consumeRecoveryCode($user, string $code): void
{
$recoveryCodes = json_decode($user->recovery_codes, true);
foreach ($recoveryCodes as &$recoveryCode) {
if ($recoveryCode['code'] === strtoupper($code)) {
$recoveryCode['used'] = true;
break;
}
}
$user->recovery_codes = json_encode($recoveryCodes);
$user->save();
}
}
前端实现
1 Blade模板
<!-- resources/views/totp/setup.blade.php -->
<!DOCTYPE html>
<html>
<head>设置双重验证</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>设置双重验证(TOTP)</h3>
</div>
<div class="card-body">
<!-- 步骤1:显示二维码 -->
<div id="step1" class="text-center">
<h5>步骤1:使用认证器App扫描二维码</h5>
<p class="text-muted">推荐使用 Google Authenticator 或 Authy</p>
<div id="qrCodeContainer" class="mb-3">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
<div id="manualSetup" class="d-none mb-3">
<p>或者手动输入密钥:</p>
<code id="secretKey" class="p-2 bg-light rounded"></code>
</div>
<button class="btn btn-link" onclick="showManualSetup()">
无法扫码?手动设置
</button>
</div>
<!-- 步骤2:验证代码 -->
<div id="step2" class="d-none">
<h5>步骤2:输入验证码确认</h5>
<p class="text-muted">请输入认证器App显示的6位数字验证码</p>
<div class="form-group mb-3">
<input type="text"
id="verificationCode"
class="form-control form-control-lg text-center"
maxlength="6"
placeholder="000000"
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
</div>
<button class="btn btn-primary btn-lg w-100" onclick="verifyCode()">
验证并开启
</button>
<div id="errorMessage" class="alert alert-danger d-none mt-3"></div>
</div>
<!-- 步骤3:完成 -->
<div id="step3" class="d-none text-center">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h4>TOTP验证已成功开启!</h4>
<p class="text-warning">请妥善保管您的恢复码</p>
<div class="recovery-codes bg-light p-3 rounded mb-3">
<h6>恢复码(每个只能使用一次)</h6>
<div id="recoveryCodes" class="text-monospace"></div>
</div>
<button class="btn btn-secondary" onclick="downloadRecoveryCodes()">
下载恢复码
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let setupData = null;
// 获取TOTP配置信息
async function getSetupInfo() {
try {
const response = await fetch('/totp/setup/info', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
setupData = await response.json();
// 显示二维码
document.getElementById('qrCodeContainer').innerHTML =
`<img src="${setupData.qr_code}" alt="TOTP QR Code" class="img-fluid">`;
// 显示密钥
document.getElementById('secretKey').textContent = setupData.secret;
// 显示步骤2
document.getElementById('step2').classList.remove('d-none');
} catch (error) {
console.error('获取配置失败:', error);
alert('获取配置失败,请刷新页面重试');
}
}
// 显示手动设置方式
function showManualSetup() {
document.getElementById('manualSetup').classList.remove('d-none');
}
// 验证TOTP码
async function verifyCode() {
const code = document.getElementById('verificationCode').value;
const errorDiv = document.getElementById('errorMessage');
if (code.length !== 6) {
errorDiv.textContent = '请输入6位验证码';
errorDiv.classList.remove('d-none');
return;
}
try {
const response = await fetch('/totp/enable', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ code: code })
});
const result = await response.json();
if (result.success) {
// 显示完成页面
document.getElementById('step2').classList.add('d-none');
document.getElementById('step3').classList.remove('d-none');
// 显示恢复码
const codesContainer = document.getElementById('recoveryCodes');
result.recovery_codes.forEach((item) => {
const p = document.createElement('p');
p.className = 'mb-1';
p.innerHTML = `<code>${item.code}</code>`;
codesContainer.appendChild(p);
});
} else {
errorDiv.textContent = result.message;
errorDiv.classList.remove('d-none');
}
} catch (error) {
console.error('验证失败:', error);
alert('验证失败,请重试');
}
}
// 下载恢复码
function downloadRecoveryCodes() {
const codes = document.querySelectorAll('#recoveryCodes code');
let text = '将用恢复码\n\n';
codes.forEach(code => {
text += code.textContent + '\n';
});
const blob = new Blob([text], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'recovery-codes.txt';
a.click();
window.URL.revokeObjectURL(url);
}
// 页面加载时获取配置
document.addEventListener('DOMContentLoaded', getSetupInfo);
</script>
</body>
</html>
2 登录页面添加TOTP验证
<!-- resources/views/auth/totp-verify.blade.php -->
<!DOCTYPE html>
<html>
<head>双重验证</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4>双重验证</h4>
</div>
<div class="card-body">
<p>请输入您的认证器App上的6位验证码</p>
<form onsubmit="verifyLoginTotp(event)">
<div class="form-group mb-3">
<input type="text"
id="code"
class="form-control form-control-lg text-center"
maxlength="6"
placeholder="000000"
autofocus>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">
验证
</button>
</form>
<div class="mt-3">
<a href="#" onclick="useRecoveryCode()">使用恢复码</a>
</div>
<div id="error" class="alert alert-danger d-none mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const userId = {{ session('2fa:user_id') }};
async function verifyLoginTotp(event) {
event.preventDefault();
const code = document.getElementById('code').value;
const errorDiv = document.getElementById('error');
if (code.length !== 6) {
errorDiv.textContent = '请输入6位验证码';
errorDiv.classList.remove('d-none');
return;
}
try {
const response = await fetch('/totp/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
code: code,
user_id: userId
})
});
const result = await response.json();
if (result.success) {
window.location.href = '/dashboard';
} else {
errorDiv.textContent = result.message;
errorDiv.classList.remove('d-none');
}
} catch (error) {
console.error('验证失败:', error);
alert('验证失败,请重试');
}
}
function useRecoveryCode() {
alert('请输入恢复码代替TOTP验证码即可');
}
</script>
</body>
</html>
数据库迁移
<?php
// database/migrations/xxxx_xx_xx_add_totp_to_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTotpToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('totp_secret')->nullable()->after('password');
$table->boolean('totp_enabled')->default(false)->after('totp_secret');
$table->text('recovery_codes')->nullable()->after('totp_enabled');
$table->timestamp('totp_activated_at')->nullable()->after('recovery_codes');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'totp_secret',
'totp_enabled',
'recovery_codes',
'totp_activated_at'
]);
});
}
}
路由配置
// routes/web.php
Route::middleware(['auth'])->group(function () {
Route::get('/totp/setup', 'TotpController@showSetupForm')->name('totp.setup');
Route::post('/totp/setup/info', 'TotpController@getSetupInfo');
Route::post('/totp/enable', 'TotpController@enableTotp');
});
// 登录后的TOTP验证
Route::post('/totp/verify', 'TotpController@verifyTotpLogin');
安全建议
- HTTPS支持:确保所有请求都在HTTPS下进行
- 速率限制:对验证码输入进行速率限制
- 会话管理:验证成功后清除临时会话数据
- 恢复码存储:建议用户下载并安全保存恢复码
- 密钥轮换:支持用户重置TOTP密钥
- 日志记录:记录所有验证尝试
这个实现提供了完整的TOTP认证流程,包括扫码绑定、验证码验证和恢复码功能。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播