Реализация confirm-механизмов в PHP для аутентификации

Раздел: Веб-разработка -> Аутентификация пользователей

Подтверждение действий пользователя - важная часть аутентификации в веб-приложениях. Оно позволяет убедиться, что пользователь является владельцем указанного email или телефона, а также предотвращает несанкционированные операции. В PHP существует несколько подходов к реализации confirm-механизмов. Рассмотрим основные варианты.

Подтверждение через email с токеном в базе данных

Цель: надёжная верификация email адреса с возможностью отслеживания статуса.

Стандартный метод: после регистрации генерируется случайный токен, сохраняется в таблице users, отправляется ссылка пользователю. При переходе по ссылке токен проверяется, и email отмечается как подтверждённый.

Как реализовать подтверждение регистрации с хранением токена в MySQL?

// Генерация токена
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));

// Сохранение в БД (PDO)
$stmt = $pdo->prepare('UPDATE users SET verification_token = :token, token_expires_at = :expires WHERE email = :email');
$stmt->execute(['token' => $token, 'expires' => $expires, 'email' => $email]);

// Отправка письма (упрощённо)
$link = "https://example.com/verify?token=$token&email=$email";
mail($email, 'Подтверждение регистрации', "Перейдите по ссылке: $link");

Confirm php (подтверждение на php)

Результат: письмо с ссылкой вида https://example.com/verify?token=abc123...&email=user@example.com

Шаги обработки ссылки:

$token = $_GET['token'] ?? '';
$email = $_GET['email'] ?? '';

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND verification_token = :token AND email_verified_at IS NULL AND (token_expires_at IS NULL OR token_expires_at > NOW())');
$stmt->execute(['email' => $email, 'token' => $token]);
$user = $stmt->fetch();

if ($user) {
    $pdo->prepare('UPDATE users SET email_verified_at = NOW(), verification_token = NULL, token_expires_at = NULL WHERE id = :id')
        ->execute(['id' => $user['id']]);
    echo 'Email подтверждён.';
} else {
    echo 'Неверная или просроченная ссылка.';
}

Типичные проблемы:

  • Токен должен быть криптостойким (используйте random_bytes).
  • Не забывайте проверять срок действия токена.
  • После подтверждения удаляйте токен, чтобы его нельзя было использовать повторно.
  • Защита от race condition - используйте транзакции или уникальные индексы.

Альтернативные варианты:

Как подтвердить email без хранения токена в БД? Использование подписанных URL (HMAC)

Цель: снизить нагрузку на БД и упростить архитектуру.

Токен формируется из данных пользователя и секретного ключа с помощью HMAC. Ссылка содержит подпись, которая проверяется на сервере без обращения к БД. Такой подход не позволяет отозвать отдельные ссылки, но подходит для систем с небольшим числом пользователей.

// Генерация подписанной ссылки
$secret = 'my-secret-key';
$data = $email . '|' . time();
$signature = hash_hmac('sha256', $data, $secret);
$link = "https://example.com/verify?data=" . urlencode($data) . "&sig=$signature";
Результат: https://example.com/verify?data=user%40example.com%7C1717171717&sig=abc123...
// Проверка
$decoded = base64_decode($_GET['data']);
list($email, $timestamp) = explode('|', $decoded);
$expected = hash_hmac('sha256', $decoded, $secret);
if (hash_equals($expected, $_GET['sig']) && $timestamp > time() - 86400) {
    // Подтверждаем email
} else {
    // Ошибка
}

Проблемы: секретный ключ должен быть надёжным, нет возможности отдельно отозвать ссылку, истечение срока задаётся жёстко. Также нужно кодировать данные (например, base64 или json).

Как реализовать подтверждение через одноразовый код (OTP)?

Цель: верификация по короткому коду, удобно для телефона или чата.

Генерируется случайный 6-значный код, сохраняется в БД с временем жизни (5-10 минут). Пользователь вводит код на странице. Код удаляется после использования.

// Генерация
$code = sprintf('%06d', random_int(0, 999999));
$_SESSION['otp'] = [
    'code' => password_hash($code, PASSWORD_DEFAULT),
    'email' => $email,
    'expires' => time() + 300
];
sendEmail($email, "Ваш код: $code");

// Проверка
$input = $_POST['code'] ?? '';
$stored = $_SESSION['otp'] ?? [];
if (time() < $stored['expires'] && $stored['email'] === $email && password_verify($input, $stored['code'])) {
    // Подтверждение
    unset($_SESSION['otp']);
} else {
    // Ошибка
}

Проблемы: необходимо ограничить количество попыток (блокировка на время). Код должен быть достаточно длинным, чтобы избежать перебора. Хэширование кода при хранении предотвращает утечку.

Как использовать passwordless подтверждение (магическая ссылка)?

Цель: исключить запоминание пароля, вход по одноразовой ссылке.

Пользователь вводит email, получает ссылку с токеном, которая автоматически аутентифицирует его. Токен одноразовый и имеет короткое время жизни. Для аутентификации можно не требовать пароль.

// Генерация
$token = bin2hex(random_bytes(32));
$stmt = $pdo->prepare('INSERT INTO login_tokens (email, token, expires_at) VALUES (?, ?, ?)');
$stmt->execute([$email, password_hash($token, PASSWORD_DEFAULT), date('Y-m-d H:i:s', time() + 3600)]);
$link = "https://example.com/login?token=$token&email=$email";
// Отправка письма

// Проверка
$stmt = $pdo->prepare('SELECT * FROM login_tokens WHERE email = ? AND expires_at > NOW()');
$stmt->execute([$email]);
$row = $stmt->fetch();
if ($row && password_verify($_GET['token'], $row['token'])) {
    // Аутентификация
    $pdo->prepare('DELETE FROM login_tokens WHERE id = ?')->execute([$row['id']]);
    // Установка сессии
}

Проблемы: если email перехвачен, злоумышленник может войти. Необходимо использовать HTTPS и предупреждать пользователя о входе с нового устройства.

Выбор метода зависит от требований безопасности, удобства пользователей и инфраструктуры проекта. Основное решение с токеном в БД - надёжный универсальный вариант, подходящий для большинства приложений.

Расширенные примеры и продвинутые техники

Пример 1: Отправка письма с подтверждением через PHPMailer и HTML шаблон

Пример
use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = 'user@example.com';
$mail->Password = 'secret';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;

$mail->setFrom('noreply@example.com', 'Support');
$mail->addAddress($email);
$mail->isHTML(true);
$mail->Subject = 'Подтверждение регистрации';
$mail->Body = '<h1>Здравствуйте!</h1><p>Для подтверждения перейдите по <a href="' . $link . '">ссылке</a></p>';
$mail->AltBody = 'Для подтверждения перейдите: ' . $link;

$mail->send();
Письмо отправлено с HTML разметкой

Шаблоны можно вынести в отдельные файлы и передавать переменные через замену плейсхолдеров.

Пример 2: Использование JWT для подписанных ссылок

Пример
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$secret = 'secret-key';
$payload = [
    'email' => $email,
    'iat' => time(),
    'exp' => time() + 3600
];
$token = JWT::encode($payload, $secret, 'HS256');
$link = "https://example.com/verify?token=$token";

// Проверка
try {
    $decoded = JWT::decode($_GET['token'], new Key($secret, 'HS256'));
    $email = $decoded->email;
    // Подтверждение
} catch (\Exception $e) {
    // Ошибка
}
Ссылка содержит JWT токен, который декодируется без обращения к БД

Преимущество JWT - стандартизация, возможность включения дополнительных данных (например, роль). Недостаток - невозможность отозвать до истечения срока.

Пример 3: Обработка повторной отправки письма с ограничением

Пример
$lastSent = $_SESSION['last_verification_sent'] ?? 0;
if (time() - $lastSent < 60) {
    echo 'Письмо уже отправлено. Повторите через минуту.';
    exit;
}
$_SESSION['last_verification_sent'] = time();
// Повторная генерация токена и отправка
Пользователь видит сообщение об ограничении

Пример 4: Логирование попыток подтверждения

Пример
$log = [
    'email' => $email,
    'ip' => $_SERVER['REMOTE_ADDR'],
    'time' => date('Y-m-d H:i:s'),
    'status' => 'success' // или 'failed'
];
file_put_contents('verification.log', json_encode($log) . PHP_EOL, FILE_APPEND);
Запись добавлена в лог-файл

Пример 5: Использование очередей для отправки писем (Redis + Resque)

Пример
require_once 'vendor/autoload.php';
use chillerlan\Queue\Queue;
use chillerlan\Queue\Jobs\DelayedJob;

$queue = new Queue('redis://localhost');
$job = new DelayedJob('sendVerificationEmail', ['email' => $email, 'token' => $token], 0);
$queue->add($job);
Письмо будет отправлено асинхронно

Подтверждение на PHP - comments

En
Confirm php (php)