← Назад к вопросам
Приведи пример решения проблемы при генерации СМС-кода в двухфакторной аутентификации
1.0 Junior🔥 131 комментариев
#Аутентификация и безопасность
Комментарии (1)
🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример решения проблемы генерации SMS-кода в двухфакторной аутентификации
При реализации двухфакторной аутентификации (2FA) через SMS необходимо решить несколько ключевых проблем: безопасность генерации кода, защита от повторного использования, ограничение частоты запросов и управление временем жизни кода.
Архитектурное решение
Типичная схема работы:
- Пользователь вводит логин/пароль
- Система генерирует одноразовый код и сохраняет его хеш
- Код отправляется через SMS-шлюз
- Пользователь вводит код для подтверждения
- Система валидирует код и предоставляет доступ
Ключевые проблемы и решения
1. Генерация криптографически безопасного кода
using System.Security.Cryptography;
public class SmsCodeGenerator
{
private const int CodeLength = 6;
private const string AllowedDigits = "0123456789";
// Использование криптографически безопасного генератора
public string GenerateSecureCode()
{
using var rng = RandomNumberGenerator.Create();
var randomBytes = new byte[CodeLength];
rng.GetBytes(randomBytes);
var codeChars = new char[CodeLength];
for (int i = 0; i < CodeLength; i++)
{
// Преобразование байта в цифру 0-9
codeChars[i] = AllowedDigits[randomBytes[i] % AllowedDigits.Length];
}
return new string(codeChars);
}
// Альтернативный вариант с ограниченным диапазоном
public string GenerateNumericCode()
{
using var rng = RandomNumberGenerator.Create();
var randomNumber = new byte[4];
rng.GetBytes(randomNumber);
// Генерация числа в диапазоне 000000-999999
int numericValue = Math.Abs(BitConverter.ToInt32(randomNumber, 0)) % 1000000;
return numericValue.ToString("D6");
}
}
2. Хранение и валидация кодов
using System;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Cryptography;
using System.Text;
public class SmsCodeService
{
private readonly IMemoryCache _cache;
private readonly TimeSpan _codeLifetime = TimeSpan.FromMinutes(5);
private readonly TimeSpan _resendCooldown = TimeSpan.FromSeconds(30);
public SmsCodeService(IMemoryCache cache)
{
_cache = cache;
}
public (string Code, DateTime ExpiresAt) GenerateAndStoreCode(string userId, string phoneNumber)
{
// Проверка cooldown для предотвращения флуда
var cooldownKey = $"cooldown:{userId}:{phoneNumber}";
if (_cache.TryGetValue(cooldownKey, out _))
{
throw new InvalidOperationException("Пожалуйста, подождите перед запросом нового кода");
}
// Генерация кода
var code = GenerateSecureCode();
var expiresAt = DateTime.UtcNow.Add(_codeLifetime);
// Ключи для хранения
var codeKey = $"smscode:{userId}:{phoneNumber}";
var attemptsKey = $"attempts:{userId}:{phoneNumber}";
// Хранение хеша кода, а не самого кода
var codeHash = HashCode(code);
var codeData = new CodeData
{
Hash = codeHash,
ExpiresAt = expiresAt,
CreatedAt = DateTime.UtcNow,
Attempts = 0
};
_cache.Set(codeKey, codeData, _codeLifetime);
_cache.Set(cooldownKey, true, _resendCooldown);
_cache.Set(attemptsKey, 0, _codeLifetime);
return (code, expiresAt);
}
public bool ValidateCode(string userId, string phoneNumber, string inputCode)
{
var codeKey = $"smscode:{userId}:{phoneNumber}";
var attemptsKey = $"attempts:{userId}:{phoneNumber}";
if (!_cache.TryGetValue<CodeData>(codeKey, out var codeData))
{
return false; // Код не найден или истек
}
// Проверка количества попыток
if (!_cache.TryGetValue<int>(attemptsKey, out var attempts))
{
attempts = 0;
}
if (attempts >= 3) // Максимум 3 попытки
{
_cache.Remove(codeKey); // Инвалидация кода
return false;
}
// Сравнение хешей
var inputHash = HashCode(inputCode);
var isValid = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(inputHash),
Encoding.UTF8.GetBytes(codeData.Hash)
);
// Увеличение счетчика попыток
_cache.Set(attemptsKey, attempts + 1, _codeLifetime);
if (isValid)
{
// Удаление кода после успешной проверки
_cache.Remove(codeKey);
_cache.Remove(attemptsKey);
}
return isValid;
}
private string GenerateSecureCode()
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[4];
rng.GetBytes(bytes);
return (Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000).ToString("D6");
}
private string HashCode(string code)
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(code));
return Convert.ToBase64String(hÿashBytes);
}
private class CodeData
{
public string Hash { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public int Attempts { get; set; }
}
}
3. Защита от атак
Меры защиты, которые необходимо реализовать:
- Rate Limiting: Ограничение количества запросов с одного IP/пользователя
- Инвалидация после использования: Код можно использовать только один раз
- Таймаут между запросами: Защита от SMS-флуда
- Ограничение попыток ввода: Блокировка после N неудачных попыток
- Ведение логов: Мониторинг подозрительной активности
public class RateLimiter
{
private readonly IMemoryCache _cache;
public bool IsAllowed(string key, int maxAttempts, TimeSpan window)
{
var cacheKey = $"ratelimit:{key}";
if (!_cache.TryGetValue<RateLimitData>(cacheKey, out var data))
{
data = new RateLimitData
{
Attempts = 1,
FirstAttempt = DateTime.UtcNow
};
_cache.Set(cacheKey, data, window);
return true;
}
if (data.Attempts >= maxAttempts)
{
return false; // Превышен лимит
}
data.Attempts++;
_cache.Set(cacheKey, data, window);
return true;
}
private class RateLimitData
{
public int Attempts { get; set; }
public DateTime FirstAttempt { get; set; }
}
}
Интеграция с SMS-сервисом
public interface ISmsSender
{
Task<bool> SendAsync(string phoneNumber, string message);
}
public class SmsCodeManager
{
private readonly SmsCodeService _codeService;
private readonly ISmsSender _smsSender;
private readonly ILogger<SmsCodeManager> _logger;
public async Task<bool> SendCodeAsync(string userId, string phoneNumber)
{
try
{
var (code, expiresAt) = _codeService.GenerateAndStoreCode(userId, phoneNumber);
var message = $"Ваш код подтверждения: {code}. Действителен до {expiresAt:HH:mm}";
var sent = await _smsSender.SendAsync(phoneNumber, message);
if (sent)
{
_logger.LogInformation("SMS код отправлен для {UserId} на {PhoneNumber}",
userId, phoneNumber);
return true;
}
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка отправки SMS кода для {UserId}", userId);
return false;
}
}
}
Лучшие практики
- Не использовать последовательные коды или легко предсказуемые паттерны
- Всегда хэшировать коды перед хранением
- Реализовать механизм очистки просроченных кодов
- Использовать распределенные блокировки в микросервисной архитектуре
- Верифицировать номер телефона перед отправкой кода
- Предоставлять одинаковые сообщения об ошибке для предотвращения перебора
- Логировать все попытки для анализа безопасности
Альтернативные подходы
Для повышенной безопасности рассмотрите:
- TOTP-коды (Time-based One-Time Password)
- Push-уведомления в мобильное приложение
- Аппаратные токены для критически важных систем
- Резервные коды для восстановления доступа
Это решение обеспечивает баланс между безопасностью, пользовательским опытом и производительностью, что критически важно для систем двухфакторной аутентификации.