feat: добавил поддержку ограничения частоты запросов
Backend CI / build-and-test (push) Successful in 48s
Frontend CI / build-and-check (push) Failing after 23s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m21s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 30s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 7s

This commit is contained in:
2026-06-01 12:40:28 +03:00
parent 450f2e2418
commit 7050851bd4
5 changed files with 119 additions and 0 deletions
+61
View File
@@ -1,12 +1,16 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
using Prometheus;
using Quartz;
using Serilog;
using System.Threading.RateLimiting;
using UniVerse.Api.BackgroundServices;
using UniVerse.Api.Filters;
using UniVerse.Api.Middleware;
@@ -69,6 +73,50 @@ builder.Services.AddAuthentication(options =>
});
builder.Services.AddAuthorization();
builder.Services.AddOptions<RateLimitingOptions>()
.Bind(builder.Configuration.GetSection(RateLimitingOptions.SectionName))
.Validate(options => options.PermitLimit >= 1,
"RateLimiting:PermitLimit must be greater than or equal to 1.")
.Validate(options => options.WindowSeconds >= 1,
"RateLimiting:WindowSeconds must be greater than or equal to 1.")
.Validate(options => options.QueueLimit >= 0,
"RateLimiting:QueueLimit must be greater than or equal to 0.")
.ValidateOnStart();
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var rateLimitingOptions = context.RequestServices.GetRequiredService<IOptions<RateLimitingOptions>>().Value;
return RateLimitPartition.GetFixedWindowLimiter(
GetRateLimitPartitionKey(context),
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = rateLimitingOptions.PermitLimit,
Window = TimeSpan.FromSeconds(rateLimitingOptions.WindowSeconds),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = rateLimitingOptions.QueueLimit,
AutoReplenishment = true
});
});
options.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
context.HttpContext.Response.ContentType = "application/problem+json";
await context.HttpContext.Response.WriteAsJsonAsync(new
{
type = "https://httpstatuses.com/429",
title = "Too Many Requests",
status = StatusCodes.Status429TooManyRequests,
detail = "Rate limit exceeded. Please try again later.",
traceId = context.HttpContext.TraceIdentifier
}, cancellationToken);
};
});
// --- CORS ---
builder.Services.AddCors(options =>
{
@@ -221,6 +269,7 @@ if (app.Environment.IsDevelopment())
app.UseCors();
app.UseAuthentication();
app.UseRateLimiter();
app.UseAuthorization();
app.UseHttpMetrics();
if (app.Environment.IsDevelopment())
@@ -237,3 +286,15 @@ app.UseWhen(
app.MapMetrics();
app.Run();
static string GetRateLimitPartitionKey(HttpContext context)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? context.User.FindFirstValue("sub");
if (!string.IsNullOrWhiteSpace(userId))
return $"user:{userId}";
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
return string.IsNullOrWhiteSpace(ipAddress) ? "anonymous:unknown" : $"ip:{ipAddress}";
}