Где хранить информацию о первом проходе при генерации временного СМС-кода?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Подход к хранению состояния «первого прохода» при генерации временного СМС-кода
При генерации временного СМС-кода, особенно в сценариях верификации (регистрация, сброс пароля, подтверждение операции), часто требуется отслеживать, является ли текущий запрос первым или повторным. Это важно для:
- Ограничения частоты запросов (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 режиме
- Нет требований к отказоустойчивости состояния
- Максимальная производительность критична
- Простота реализации приоритетнее масштабируемости
Дополнительные рекомендации
-
Безопасность: Никогда не храните сгенерированный код в открытом виде. Используйте хеширование (например, bcrypt) если код должен быть сохранен для последующей проверки.
-
Очистка старых данных: Настройте автоматическое удаление устаревших записей либо через TTL в Redis, либо фоновым job в БД.
-
Лимиты и защита: Реализуйте лимиты на количество запросов в единицу времени:
// Пример с 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();
- Идемпотентность: Сделайте обработку запроса идемпотентной, чтобы повторные одинаковые запросы не создавали новые коды без необходимости.
Итоговый выбор: Для большинства современных облачных приложений рекомендую использовать Redis как баланс между производительностью, масштабируемостью и простотой реализации. Он идеально подходит для временных данных с TTL и обеспечивает консистентность состояния across multiple instances.