/
ivanstrike
/
tasker
Обзор
Документация
Войти
/
ivanstrike
/
tasker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Middleware/RateLimitingMiddleware.cs
193 строки
6 KB
ivanstrike
Лабораторная работа 2
08 ноя 2025, 11:16
08 ноя 2025, 11:16
ec39293
Код
Авторство
О чём код?
using System.Collections.Concurrent; using System.Security.Claims; using Microsoft.EntityFrameworkCore; using TaskerApi.Data; using TaskerApi.Models; namespace TaskerApi.Middleware; /// <summary> /// Middleware для ограничения частоты запросов (Rate Limiting) /// </summary> public class RateLimitingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RateLimitingMiddleware> _logger; private readonly int _requestsPerMinute; private readonly int _requestsPerHour; private static readonly ConcurrentDictionary<string, (DateTime WindowStart, int Count)> _cache = new(); public RateLimitingMiddleware( RequestDelegate next, ILogger<RateLimitingMiddleware> logger, IConfiguration configuration) { _next = next; _logger = logger; _requestsPerMinute = configuration.GetValue<int>("RateLimiting:RequestsPerMinute", 60); _requestsPerHour = configuration.GetValue<int>("RateLimiting:RequestsPerHour", 1000); } public async Task InvokeAsync(HttpContext context, ApplicationDbContext dbContext) { // Пропускаем internal endpoints (они защищены API ключом) if (context.Request.Path.StartsWithSegments("/api/internal")) { await _next(context); return; } var clientId = GetClientIdentifier(context); if (string.IsNullOrEmpty(clientId)) { await _next(context); return; } var now = DateTime.UtcNow; var minuteWindowStart = now.AddMinutes(-1); if (_cache.TryGetValue(clientId, out var cacheEntry)) { if (cacheEntry.WindowStart > minuteWindowStart) { if (cacheEntry.Count >= _requestsPerMinute) { await SendRateLimitResponse(context, 0, (int)(60 - (now - cacheEntry.WindowStart).TotalSeconds)); return; } _cache[clientId] = (cacheEntry.WindowStart, cacheEntry.Count + 1); } else { _cache[clientId] = (now, 1); } } else { _cache[clientId] = (now, 1); } var hourWindowStart = now.AddHours(-1); var requestsInLastHour = await dbContext.RateLimitInfos .Where(r => r.ClientId == clientId && r.LastRequestAt > hourWindowStart) .SumAsync(r => r.RequestCount); if (requestsInLastHour >= _requestsPerHour) { var oldestRequest = await dbContext.RateLimitInfos .Where(r => r.ClientId == clientId && r.LastRequestAt > hourWindowStart) .OrderBy(r => r.LastRequestAt) .FirstOrDefaultAsync(); var retryAfter = oldestRequest != null ? (int)(3600 - (now - oldestRequest.LastRequestAt).TotalSeconds) : 3600; await SendRateLimitResponse(context, 0, retryAfter); return; } var rateLimitInfo = new RateLimitInfo { ClientId = clientId, RequestCount = 1, WindowStart = now, LastRequestAt = now }; dbContext.RateLimitInfos.Add(rateLimitInfo); await dbContext.SaveChangesAsync(); var currentEntry = _cache.TryGetValue(clientId, out var entry) ? entry : (now, 0); var currentCount = currentEntry.Item2; var windowStart = currentEntry.Item1; var remaining = Math.Max(0, _requestsPerMinute - currentCount); context.Response.OnStarting(() => { context.Response.Headers["X-RateLimit-Limit"] = _requestsPerMinute.ToString(); context.Response.Headers["X-Limit-Remaining"] = remaining.ToString(); context.Response.Headers["X-RateLimit-Remaining"] = remaining.ToString(); context.Response.Headers["X-RateLimit-Reset"] = new DateTimeOffset(windowStart.AddMinutes(1)).ToUnixTimeSeconds().ToString(); if (remaining == 0 && context.Response.StatusCode != 429) { var retryAfter = (int)Math.Ceiling((windowStart.AddMinutes(1) - DateTime.UtcNow).TotalSeconds); context.Response.Headers["Retry-After"] = Math.Max(1, retryAfter).ToString(); } return Task.CompletedTask; }); _logger.LogInformation("Rate limit for client {ClientId}: {Remaining}/{Limit}", clientId, remaining, _requestsPerMinute); await _next(context); if (_cache.Count > 1000) { CleanupCache(); } } private string GetClientIdentifier(HttpContext context) { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { return $"user_{userId}"; } var ipAddress = context.Connection.RemoteIpAddress?.ToString(); if (!string.IsNullOrEmpty(ipAddress)) { return $"ip_{ipAddress}"; } return string.Empty; } private async Task SendRateLimitResponse(HttpContext context, int remaining, int retryAfter) { context.Response.StatusCode = 429; context.Response.Headers["X-RateLimit-Limit"] = _requestsPerMinute.ToString(); context.Response.Headers["X-RateLimit-Remaining"] = remaining.ToString(); context.Response.Headers["X-Limit-Remaining"] = remaining.ToString(); context.Response.Headers["Retry-After"] = retryAfter.ToString(); context.Response.ContentType = "application/json"; var response = new { error = "Rate limit exceeded", message = $"Too many requests. Please retry after {retryAfter} seconds.", retryAfter = retryAfter, limit = _requestsPerMinute, remaining = remaining }; await context.Response.WriteAsJsonAsync(response); _logger.LogWarning("Rate limit exceeded for client. Retry after: {RetryAfter} seconds", retryAfter); } private void CleanupCache() { var now = DateTime.UtcNow; var keysToRemove = _cache .Where(kvp => (now - kvp.Value.WindowStart).TotalMinutes > 2) .Select(kvp => kvp.Key) .ToList(); foreach (var key in keysToRemove) { _cache.TryRemove(key, out _); } _logger.LogInformation("Cleaned up {Count} old entries from rate limit cache", keysToRemove.Count); } }