← Назад к вопросам

Приведи пример решения проблемы при генерации СМС-кода в двухфакторной аутентификации

1.0 Junior🔥 131 комментариев
#Аутентификация и безопасность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Пример решения проблемы генерации SMS-кода в двухфакторной аутентификации

При реализации двухфакторной аутентификации (2FA) через SMS необходимо решить несколько ключевых проблем: безопасность генерации кода, защита от повторного использования, ограничение частоты запросов и управление временем жизни кода.

Архитектурное решение

Типичная схема работы:

  1. Пользователь вводит логин/пароль
  2. Система генерирует одноразовый код и сохраняет его хеш
  3. Код отправляется через SMS-шлюз
  4. Пользователь вводит код для подтверждения
  5. Система валидирует код и предоставляет доступ

Ключевые проблемы и решения

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;
        }
    }
}

Лучшие практики

  1. Не использовать последовательные коды или легко предсказуемые паттерны
  2. Всегда хэшировать коды перед хранением
  3. Реализовать механизм очистки просроченных кодов
  4. Использовать распределенные блокировки в микросервисной архитектуре
  5. Верифицировать номер телефона перед отправкой кода
  6. Предоставлять одинаковые сообщения об ошибке для предотвращения перебора
  7. Логировать все попытки для анализа безопасности

Альтернативные подходы

Для повышенной безопасности рассмотрите:

  • TOTP-коды (Time-based One-Time Password)
  • Push-уведомления в мобильное приложение
  • Аппаратные токены для критически важных систем
  • Резервные коды для восстановления доступа

Это решение обеспечивает баланс между безопасностью, пользовательским опытом и производительностью, что критически важно для систем двухфакторной аутентификации.