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

Где хранить информацию о первом проходе при генерации временного СМС-кода?

2.2 Middle🔥 192 комментариев
#Кэширование и Redis

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

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

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

Подход к хранению состояния «первого прохода» при генерации временного СМС-кода

При генерации временного СМС-кода, особенно в сценариях верификации (регистрация, сброс пароля, подтверждение операции), часто требуется отслеживать, является ли текущий запрос первым или повторным. Это важно для:

  • Ограничения частоты запросов (rate limiting)
  • Предотвращения спама и злоупотреблений
  • Корректного обновления кода при повторных запросах
  • Учета бизнес-логики (например, блокировка после N попыток)

Основные стратегии хранения

1. Использование распределенного кэша (предпочтительный способ)

Для современных распределенных приложений (особенно с несколькими экземплярами backend) лучшим решением является распределенный кэш, такой как Redis или Memcached. Он обеспечивает:

  • Общее состояние для всех инстансов приложения
  • Автоматическое удаление данных по TTL (совпадает со временем жизни кода)
  • Высокую производительность
// Пример с использованием Redis через StackExchange.Redis
public class SmsCodeService
{
    private readonly IDatabase _redis;
    
    public async Task<string> GenerateOrUpdateCodeAsync(string phoneNumber)
    {
        var key = $"sms:first_attempt:{phoneNumber}";
        var existing = await _redis.StringGetAsync(key);
        
        if (existing.HasValue)
        {
            // Повторный запрос - обновляем код
            var newCode = GenerateCode();
            await _redis.StringSetAsync(key, newCode, TimeSpan.FromMinutes(5));
            return newCode;
        }
        else
        {
            // Первый запрос - создаем новый код
            var code = GenerateCode();
            await _redis.StringSetAsync(key, code, TimeSpan.FromMinutes(5));
            return code;
        }
    }
    
    private string GenerateCode() => new Random().Next(100000, 999999).ToString();
}

2. База данных с флагом или временной меткой

Подходит, если у вас уже есть таблица пользователей или отдельная таблица для кодов верификации.

CREATE TABLE SmsVerificationCodes (
    Id BIGINT PRIMARY KEY IDENTITY,
    PhoneNumber NVARCHAR(20) NOT NULL,
    Code NVARCHAR(10) NOT NULL,
    AttemptCount INT DEFAULT 1,
    FirstRequestTime DATETIME2 NOT NULL,
    LastRequestTime DATETIME2 NOT NULL,
    ExpiresAt DATETIME2 NOT NULL,
    INDEX IX_PhoneNumber (PhoneNumber)
);
// Пример Entity Framework Core
public async Task<string> HandleSmsRequestAsync(string phoneNumber)
{
    var existing = await _context.SmsCodes
        .FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
    
    if (existing == null)
    {
        // Первый запрос
        var code = new SmsCode 
        {
            PhoneNumber = phoneNumber,
            Code = GenerateCode(),
            FirstRequestTime = DateTime.UtcNow,
            AttemptCount = 1,
            ExpiresAt = DateTime.UtcNow.AddMinutes(5)
        };
        _context.SmsCodes.Add(code);
    }
    else
    {
        // Повторный запрос
        existing.AttemptCount++;
        existing.Code = GenerateCode(); // Генерируем новый код
        existing.LastRequestTime = DateTime.UtcNow;
    }
    
    await _context.SaveChangesAsync();
    return code;
}

3. In-memory кэш (только для single-instance приложений)

Используйте IMemoryCache в ASP.NET Core, если у вас одноэкземплярное приложение.

public class SmsService
{
    private readonly IMemoryCache _cache;
    
    public string GetOrCreateCode(string phoneNumber)
    {
        var cacheKey = $"sms_code_{phoneNumber}";
        
        if (_cache.TryGetValue(cacheKey, out SmsCodeInfo cached))
        {
            // Повторный запрос
            cached.AttemptCount++;
            cached.Code = GenerateCode();
            _cache.Set(cacheKey, cached, TimeSpan.FromMinutes(5));
            return cached.Code;
        }
        
        // Первый запрос
        var newCodeInfo = new SmsCodeInfo
        {
            PhoneNumber = phoneNumber,
            Code = GenerateCode(),
            FirstRequestTime = DateTime.UtcNow,
            AttemptCount = 1
        };
        
        _cache.Set(cacheKey, newCodeInfo, TimeSpan.FromMinutes(5));
        return newCodeInfo.Code;
    }
}

public class SmsCodeInfo
{
    public string PhoneNumber { get; set; }
    public string Code { get; set; }
    public DateTime FirstRequestTime { get; set; }
    public int AttemptCount { get; set; }
}

Критерии выбора подхода

Выбирайте распределенный кэш (Redis), если:

  • Приложение работает в кластере (несколько инстансов)
  • Требуется высокая доступность и отказоустойчивость
  • Нужна возможность масштабирования
  • Важна высокая производительность (миллисекундные задержки)

Выбирайте базу данных, если:

  • Требуется персистентное хранение для аудита
  • Нужны сложные запросы или отчетность
  • Уже используете транзакции в связанных операциях
  • Код должен пережить перезапуск кэша

Выбирайте in-memory кэш, если:

  • Приложение работает в single-instance режиме
  • Нет требований к отказоустойчивости состояния
  • Максимальная производительность критична
  • Простота реализации приоритетнее масштабируемости

Дополнительные рекомендации

  1. Безопасность: Никогда не храните сгенерированный код в открытом виде. Используйте хеширование (например, bcrypt) если код должен быть сохранен для последующей проверки.

  2. Очистка старых данных: Настройте автоматическое удаление устаревших записей либо через TTL в Redis, либо фоновым job в БД.

  3. Лимиты и защита: Реализуйте лимиты на количество запросов в единицу времени:

// Пример с Redis для rate limiting
var key = $"rate_limit:{phoneNumber}:{DateTime.UtcNow:yyyyMMddHH}";
var current = await _redis.IncrementAsync(key);
if (current == 1) await _redis.ExpireAsync(key, TimeSpan.FromHours(1));
if (current > 5) throw new RateLimitExceededException();
  1. Идемпотентность: Сделайте обработку запроса идемпотентной, чтобы повторные одинаковые запросы не создавали новые коды без необходимости.

Итоговый выбор: Для большинства современных облачных приложений рекомендую использовать Redis как баланс между производительностью, масштабируемостью и простотой реализации. Он идеально подходит для временных данных с TTL и обеспечивает консистентность состояния across multiple instances.