Разработка на PHP: Как создать безопасный токен для сброса пароля пользователя и сохранить его в БД с ограниченным временем жизни?
Создание безопасного токена для сброса пароля — важная задача в веб-разработке. Неправильная реализация может привести к захвату аккаунтов пользователей. Разберём весь процесс пошагово.
## 1. Генерация криптографически стойкого токена
Никогда не используйте `rand()`, `mt_rand()` или `uniqid()` для генерации токенов — они предсказуемы. Используйте `random_bytes()` или `bin2hex(random_bytes())`:
php
function generateResetToken(): string {
return bin2hex(random_bytes(32)); // 64 символа, 256 бит энтропии
}
Эта функция доступна начиная с PHP 7 и является криптографически безопасной.
## 2. Хеширование токена перед сохранением
Токен, который отправляется пользователю, не должен храниться в БД в открытом виде. Если злоумышленник получит доступ к базе данных, он не должен иметь возможности использовать токены:
php
$token = generateResetToken();
$tokenHash = hash(‘sha256’, $token); // Хешируем перед сохранением
Пользователю отправляете `$token`, а в БД сохраняете `$tokenHash`.
## 3. Структура таблицы в базе данных
sql
CREATE TABLE password_reset_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token_hash VARCHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
used_at DATETIME DEFAULT NULL,
UNIQUE KEY unique_token (token_hash),
INDEX idx_user_id (user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
## 4. Сохранение токена с ограниченным временем жизни
php
function saveResetToken(PDO $pdo, int $userId, string $tokenHash, int $ttlMinutes = 60): void {
// Удаляем старые токены пользователя
$stmt = $pdo->prepare(‘DELETE FROM password_reset_tokens WHERE user_id = ?’);
$stmt->execute([$userId]);
// Сохраняем новый токен
$expiresAt = date(‘Y-m-d H:i:s’, time() + $ttlMinutes * 60);
$stmt = $pdo->prepare(
‘INSERT INTO password_reset_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)’
);
$stmt->execute([$userId, $tokenHash, $expiresAt]);
}
## 5. Проверка токена при сбросе пароля
php
function verifyResetToken(PDO $pdo, string $token): ?int {
$tokenHash = hash(‘sha256’, $token);
$stmt = $pdo->prepare(
‘SELECT user_id FROM password_reset_tokens
WHERE token_hash = ? AND expires_at > NOW() AND used_at IS NULL’
);
$stmt->execute([$tokenHash]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? (int)$row[‘user_id’] : null;
}
function markTokenAsUsed(PDO $pdo, string $token): void {
$tokenHash = hash(‘sha256’, $token);
$stmt = $pdo->prepare(
‘UPDATE password_reset_tokens SET used_at = NOW() WHERE token_hash = ?’
);
$stmt->execute([$tokenHash]);
}
## 6. Полный сценарий использования
php
// Шаг 1: Запрос на сброс
$token = generateResetToken();
$tokenHash = hash(‘sha256’, $token);
saveResetToken($pdo, $userId, $tokenHash, 60); // TTL = 60 минут
sendResetEmail($userEmail, $token); // Отправляем $token пользователю
// Шаг 2: Пользователь переходит по ссылке
$userId = verifyResetToken($pdo, $_GET[‘token’]);
if (!$userId) {
die(‘Токен недействителен или истёк.’);
}
// Шаг 3: Смена пароля
$newPasswordHash = password_hash($_POST[‘password’], PASSWORD_BCRYPT);
// Обновляем пароль в БД…
markTokenAsUsed($pdo, $_GET[‘token’]); // Инвалидируем токен
## Дополнительные рекомендации
— **Ограничивайте количество запросов** на сброс пароля с одного IP или для одного email (rate limiting).
— **Не раскрывайте**, существует ли email в системе — всегда возвращайте одинаковое сообщение.
— **Периодически очищайте** устаревшие токены через cron-задание: `DELETE FROM password_reset_tokens WHERE expires_at < NOW()`.
— Используйте **HTTPS** для передачи токена в URL.
— Рекомендуемый TTL — от 15 до 60 минут.
