ASP.NET Core의 속도 제한 미들웨어

아르빈 카바지, 마틴 발리오우, 릭 앤더슨

Microsoft.AspNetCore.RateLimiting 미들웨어는 속도 제한 미들웨어를 제공합니다. 앱은 속도 제한 정책을 구성한 다음, 정책을 엔드포인트에 연결합니다. 속도 제한을 사용하는 앱은 배포하기 전에 신중하게 부하를 테스트하고 검토해야 합니다. 자세한 내용은 이 문서의 속도 제한을 사용한 엔드포인트 테스트를 참조하세요.

속도 제한에 대한 소개는 속도 제한 미들웨어를 참조 하세요.

속도 제한을 사용하는 이유

속도 제한은 앱에 들어오는 요청의 흐름을 관리하는 데 사용할 수 있습니다. 속도 제한을 구현하는 주요 이유:

  • 남용 방지: 속도 제한은 사용자 또는 클라이언트가 지정된 기간 동안 수행할 수 있는 요청 수를 제한하여 앱의 남용을 방지하는 데 도움이 됩니다. 이 보호는 공용 API에 특히 중요합니다.

  • 공정한 사용 보장: 사용자가 시스템을 독점하지 못하도록 제한을 설정하여 모든 사용자가 리소스에 공평하게 액세스할 수 있도록 합니다.

  • 리소스 보호: 속도 제한은 처리할 수 있는 요청 수를 제어하여 서버 오버로드를 방지하는 데 도움이 됩니다. 백 엔드 리소스가 과부하되지 않도록 보호합니다.

  • 보안 강화: 요청이 처리되는 속도를 제한하여 DoS(서비스 거부) 공격의 위험을 완화할 수 있습니다. 공격자가 시스템에 과도한 요청을 보내기 어렵게 만듭니다.

  • 성능 향상: 들어오는 요청의 속도를 제어하여 앱의 최적의 성능과 응답성을 유지하여 더 나은 사용자 환경을 보장할 수 있습니다.

  • Cost Management: 사용량에 따라 비용이 발생하는 서비스의 경우 속도 제한은 처리된 요청의 양을 제어하여 비용을 관리하고 예측하는 데 도움이 될 수 있습니다.

ASP.NET Core 앱에서 속도 제한을 구현하면 안정성, 보안 및 성능을 유지하는 데 도움이 될 수 있습니다. 결과는 모든 사용자에게 안정적이고 효율적인 서비스입니다.

DDoS 공격 방지

속도 제한은 요청이 처리되는 속도를 제한하여 DoS(서비스 거부) 공격의 위험을 완화하는 데 도움이 될 수 있지만 DDoS(분산 서비스 거부) 공격에 대한 포괄적인 솔루션은 아닙니다. DDoS 공격에는 요청이 많은 앱을 압도하는 여러 시스템이 포함되므로 속도 제한만으로는 처리하기가 어렵습니다.

강력한 DDoS 보호를 위해 상업용 DDoS 보호 서비스를 사용하는 것이 좋습니다. 이러한 서비스는 다음과 같은 고급 기능을 제공합니다.

  • 트래픽 분석: 들어오는 트래픽을 지속적으로 모니터링하고 분석하여 DDoS 공격을 실시간으로 감지하고 완화합니다.
  • 확장성: 여러 서버 및 데이터 센터에 트래픽을 분산하여 대규모 공격을 처리하는 기능입니다.
  • 자동화된 완화: 수동 개입 없이 악의적인 트래픽을 빠르게 차단하는 자동화된 응답 메커니즘입니다.
  • 글로벌 네트워크: 원본에 가까운 공격을 흡수하고 완화하는 서버의 글로벌 네트워크입니다.
  • 지속적인 업데이트: 상용 서비스는 새롭고 진화하는 위협에 맞게 보호 메커니즘을 지속적으로 추적하고 업데이트합니다.

클라우드 호스팅 서비스를 사용하는 경우 DDoS 보호는 일반적으로 Azure Web Application Firewall , AWS Shield 또는 Google Cloud Armor 같은 호스팅 솔루션의 일부로 사용할 수 있습니다. 전용 보호는 WAF(웹 애플리케이션 방화벽) 또는 Cloudflare 또는 Akamai Kona Site Defender와 같은 CDN 솔루션의 일부로 사용할 수

속도 제한과 함께 상용 DDoS 보호 서비스를 구현하면 포괄적인 방어 전략을 제공하여 앱의 안정성, 보안 및 성능을 보장할 수 있습니다.

속도 제한 미들웨어 사용

다음 단계에서는 ASP.NET Core 앱에서 속도 제한 미들웨어를 사용하는 방법을 보여줍니다.

  1. 속도 제한 서비스를 구성합니다.

Program.cs 파일에서 적절한 속도 제한 정책을 추가하여 속도 제한 서비스를 구성합니다. 정책을 전역 또는 명명된 정책으로 정의합니다. 다음 예제에서는 사용자(ID) 또는 전역적으로 분당 10개의 요청을 허용합니다.

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

명명된 정책은 페이지 또는 엔드포인트에 명시적으로 적용해야 합니다. 다음 예제에서는 나중에 엔드포인트에 추가하는 명명 "fixed" 된 고정 창 제한기 정책을 추가합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 4;
        opt.Window = TimeSpan.FromSeconds(12);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;
    });
});

var app = builder.Build();

전역 리미터는 옵션을 통해 구성할 때 모든 엔드포인트에 자동으로 적용됩니다 . GlobalLimiter.

  1. 속도 제한 미들웨어 활성화

    Program.cs 파일에서 UseRateLimiter호출하여 속도 제한 미들웨어를 사용하도록 설정합니다.

app.UseRouting();

app.UseRateLimiter();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Run();

엔드포인트 또는 페이지에 속도 제한 정책 적용

Web API 엔드포인트에 속도 제한 적용

엔드포인트 또는 그룹에 명명된 정책을 적용합니다. 예를 들면 다음과 같습니다.


app.MapGet("/api/resource", () => "This endpoint is rate limited")
   .RequireRateLimiting("fixed"); // Apply specific policy to an endpoint

MVC 컨트롤러에 속도 제한 적용

구성된 속도 제한 정책을 특정 엔드포인트 또는 전역적으로 적용합니다. 예를 들어 모든 컨트롤러 엔드포인트에 "고정" 정책을 적용하려면 다음을 수행합니다.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireRateLimiting("fixed");
});

서버 쪽 Blazor 앱에 속도 제한 적용

모든 앱의 라우팅 가능한 Razor 구성 요소에 대한 속도 제한을 설정하려면 RequireRateLimiting 파일의 MapRazorComponents 호출에서 속도 제한 정책 이름으로 Program 지정합니다. 다음 예제에서는 "policy"라는 속도 제한 정책이 적용됩니다.

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .RequireRateLimiting("policy");

가져오기 파일()_Imports.razor을 통해 단일 라우팅 가능 Razor 구성 요소 또는 구성 요소 폴더에 대한 정책을 설정하려면 정책 이름으로 특성을 적용 [EnableRateLimiting] 합니다. 다음 예제에서는 "override"이라는 속도 제한 정책이 적용됩니다. 정책은 현재 엔드포인트에 적용된 모든 정책을 대체합니다. 전역 리미터는 이 특성이 적용된 엔드포인트에서 계속 실행됩니다.

@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]

<h1>Counter</h1>

에서 RequireRateLimiting[EnableRateLimiting] 않는 경우에만 특성을 imports 파일을 통해 라우팅 가능한 구성 요소 또는 구성 요소 폴더에 적용합니다.

[DisableRateLimiting] 이 특성을 사용하여 가져오기 파일을 통해 라우팅 가능한 구성 요소 또는 구성 요소 폴더에 대한 속도 제한을 사용하지 않도록 설정합니다.

속도 제한기 알고리즘

RateLimiterOptionsExtensions 클래스는 속도 제한에 대해 다음과 같은 확장 메서드를 제공합니다.

고정, 슬라이딩 및 토큰 제한기는 모두 지정된 기간의 최대 요청 수를 제한합니다. 동시성 제한기는 동시 요청 수만 제한하고 지정된 기간의 요청 수를 제한하지 않습니다. 한도를 선택할 때 엔드포인트의 비용을 고려합니다. 엔드포인트 비용에는 시간, 데이터 액세스, CPU 및 I/O와 같이 사용되는 리소스가 포함됩니다.

고정 창 제한기

AddFixedWindowLimiter 메서드는 고정된 시간 창을 사용하여 요청을 제한합니다. 기간이 만료되면 새 시간 창이 시작되고 요청 제한이 다시 설정됩니다.

다음 코드를 생각해 봅시다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: "fixed", options =>
    {
        options.PermitLimit = 4;
        options.Window = TimeSpan.FromSeconds(12);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 2;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
                           .RequireRateLimiting("fixed");

app.Run();

앞의 코드가 하는 역할은 다음과 같습니다.

  • AddRateLimiter를 호출하여 서비스 컬렉션에 속도 제한 서비스를 추가합니다.
  • AddFixedWindowLimiter를 호출하여 정책 이름이 "fixed"인 고정 창 제한기를 만들고 다음을 설정합니다.
  • PermitLimit을 4로, 시간 Window를 12로 지정합니다. 12초 창마다 최대 4개의 요청이 허용됩니다.
  • QueueProcessingOrderOldestFirst로 변경되었습니다.
  • QueueLimit 2로 설정합니다(큐 메커니즘을 사용하지 않도록 설정하려면 0으로 설정).
  • UseRateLimiter를 호출하여 속도 제한을 사용하도록 설정합니다.

제한기 옵션을 설정하려면 앱이 구성을 사용해야 합니다. 다음 코드는 구성에 MyRateLimitOptions를 사용하여 이전 코드를 업데이트합니다.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
                           .RequireRateLimiting(fixedPolicy);

app.Run();

속도 제한 엔드포인트 특정 API를 사용할 때는 UseRateLimiter 호출 후에 UseRouting를 호출해야 합니다. 예를 들어 [EnableRateLimiting] 특성을 사용하는 경우, UseRateLimiterUseRouting 후에 반드시 호출해야 합니다. 전역 리미터 UseRateLimiter 만 호출하는 경우 이전에 UseRouting호출할 수 있습니다.

슬라이딩 창 제한기

슬라이딩 창 알고리즘:

  • 고정 창 제한기와 비슷하지만 창당 세그먼트를 추가합니다. 창은 각 세그먼트 간격마다 세그먼트 하나를 슬라이드합니다. 세그먼트 간격은 (창 시간)/(창당 세그먼트)입니다.
  • 창에 대한 요청을 permitLimit회로 제한합니다.
  • 각 시간 창은 창당 n 세그먼트로 나뉩니다.
  • 만료된 시간 세그먼트에서 가져온 요청은 이전 창의 세그먼트(n 현재 세그먼트 이전의 세그먼트)로부터 현재 세그먼트에 추가됩니다. 창 한 개 전의 가장 만료된 시간 세그먼트를 만료된 세그먼트라고 합니다.

30초 창, 창당 3개의 세그먼트 및 100개의 요청 제한이 있는 슬라이딩 윈도우 리미터를 보여 주는 다음 표를 고려하세요.

  • 맨 위 행과 첫 번째 열에는 시간 세그먼트가 표시됩니다.
  • 두 번째 행에는 사용 가능한 나머지 요청이 표시됩니다. 나머지 요청은 처리된 요청과 재활용된 요청을 뺀 사용 가능한 요청으로 계산됩니다.
  • 요청은 매번 대각선 파란색 선을 따라 이동합니다.
  • 시간 30부터, 만료된 시간 세그먼트에서 가져온 요청은 빨간색 선으로 표시한 부분에서 알 수 있듯 요청 제한에 다시 추가됩니다.

요청, 제한 및 재활용 슬롯을 보여 주는 표

다음 표는 이전 그래프의 데이터를 다른 서식으로 보여줍니다. 사용 가능한 열은 이전 세그먼트에서 사용할 수 있는 요청(이전 행의 이월 요청을)을 보여줍니다. 첫 번째 행에는 이전 세그먼트가 없으므로 100개의 사용 가능한 요청이 표시됩니다.

시간 사용 가능 차지됨 만료된 것에서 재활용됨 이월
0 100 20 0 80
10 80 30 0 50
20 50 40 0 10
30 10 30 20 0
40 0 10 30 20
50 20 10 40 50
60 50 35 30 45

다음 코드는 슬라이딩 윈도우 속도 제한기를 사용합니다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
                           .RequireRateLimiting(slidingPolicy);

app.Run();

토큰 버킷 제한기

토큰 버킷 제한기는 슬라이딩 윈도우 제한기와 비슷하지만 만료된 세그먼트에서 가져온 요청을 다시 추가하지 않고 각 보충 기간마다 고정된 수의 토큰을 추가합니다. 각 세그먼트에 추가된 토큰은 사용 가능한 토큰을 토큰 버킷 제한보다 더 높게 늘릴 수 없습니다. 다음 표에서는 토큰이 100개 제한되고 10초의 보충 기간이 있는 토큰 버킷 리미더를 보여 줍니다.

시간 사용 가능 차지됨 추가됨 이월
0 100 20 0 80
10 80 10 20 90
20 90 5 15 100
30 100 30 20 90
40 90 6 16 100
50 100 40 20 80
60 80 50 20 50

다음 코드는 토큰 버킷 제한기를 사용합니다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddTokenBucketLimiter(policyName: tokenPolicy, options =>
    {
        options.TokenLimit = myOptions.TokenLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
        options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
        options.TokensPerPeriod = myOptions.TokensPerPeriod;
        options.AutoReplenishment = myOptions.AutoReplenishment;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
                           .RequireRateLimiting(tokenPolicy);

app.Run();

AutoReplenishmenttrue로 설정되면 내부 타이머는 ReplenishmentPeriod 때마다 토큰을 보충합니다. false로 설정된 경우 앱이 제한기에서 TryReplenish를 호출해야 합니다.

동시성 제한기

동시성 제한은 동시 요청 수를 제한합니다. 각 요청은 동시성 제한을 1씩 줄입니다. 요청이 완료되면 제한이 1씩 증가합니다. 지정된 기간 동안 총 요청 수를 제한하는 다른 요청 제한기와 달리 동시성 제한기는 동시 요청 수만 제한하고 지정된 기간의 요청 수를 제한하지 않습니다.

다음 코드는 동시성 제한기를 사용합니다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", async () =>
{
    await Task.Delay(500);
    return Results.Ok($"Concurrency Limiter {GetTicks()}");
                              
}).RequireRateLimiting(concurrencyPolicy);

app.Run();

파티션 속도 제한

속도 제한 파티션은 트래픽을 각각 고유한 속도 제한 카운터를 가져오는 별도의 버킷으로 나눕니다. 이 방법은 단일 전역 카운터보다 더 세부적인 제어를 제공합니다. 사용자 ID, IP 주소 또는 API 키와 같은 다른 키는 파티션 버킷을 정의합니다.

분할의 이점

  • 공정성: 한 사용자가 모든 사용자에 대해 전체 속도 제한을 사용할 수 없습니다.
  • 세분화: 사용자와 리소스별로 서로 다른 제한을 적용합니다.
  • 보안: 대상 남용에 대한 더 나은 보호.
  • 계층화된 서비스: 제한이 다른 서비스 계층에 대한 지원.

분할된 속도 제한을 사용하면 API 트래픽을 관리하는 방법을 세밀하게 제어하면서 공정한 리소스 할당을 보장합니다.

IP 주소별

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        }));

사용자 ID별

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        }));

API 키별

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string apiKey = httpContext.Request.Headers["X-API-Key"].ToString() ?? "no-key";

    // Different limits based on key tier
    return apiKey switch
    {
        "premium-key" => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 1000,
                Window = TimeSpan.FromMinutes(1)
            }),

        _ => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }),
    };
});

엔드포인트 경로별

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string path = httpContext.Request.Path.ToString();

    // Different limits for different paths
    if (path.StartsWith("/api/public"))
    {
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"{httpContext.Connection.RemoteIpAddress}-public",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 30,
                Window = TimeSpan.FromSeconds(10)
            });
    }

    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        });
});

연결된 리미터 만들기

API는 CreateChained 여러 PartitionedRateLimiter 인스턴스를 허용하고 하나로 PartitionedRateLimiter결합합니다. 결합된 리미터는 모든 입력 리미터를 순서대로 실행합니다. 결과는 PartitionedRateLimiter에 할당된 GlobalLimiter이므로 체인은 모든 엔드포인트에 적용됩니다. 대신 특정 엔드포인트에 대한 리미터를 연결하려면 명명된 정책의 체인 리미터에 표시된 대로 명명된 정책을 사용합니다.

다음 코드는 CreateChained를 사용합니다.

using System.Globalization;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
    _.OnRejected = async (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
    };
    _.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 4,
                    Window = TimeSpan.FromSeconds(2)
                });
        }),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();
            
            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 20,    
                    Window = TimeSpan.FromSeconds(30)
                });
        }));
});

var app = builder.Build();
app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));

app.Run();

자세한 내용은 CreateChained 소스 코드를 참조하세요.

이름이 지정된 정책의 체인 리미터

CreateChained 는 모든 엔드포인트에 적용되는 전역 리미터를 연결합니다. 여러 리미터 형식을 결합하고 특정 엔드포인트로 범위를 지정하려면 명명된 정책 내에 리미티터를 연결 CreateChained합니다. 이 오버로드는 명명된 정책의 파티션 팩터리에 필요한 반환 형식인 각 제한기를 순서대로 실행하는 단일 RateLimiter 을 반환합니다.

다음 "combined" 정책은 토큰 버킷 리미터와 동시성 제한기를 RateLimiter.CreateChained연결한 다음 다음을 사용하여 단일 엔드포인트 RequireRateLimiting에 정책을 적용합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("combined", httpContext =>
    {
        // Partition on the authenticated identity name when available. Each distinct key creates and
        // caches its own limiter, so partitioning on unbounded user-controlled
        // input can exhaust memory (a DoS risk).
        string partitionKey = httpContext.User.Identity?.Name ?? "anonymous";

        return RateLimitPartition.Get(partitionKey, _ =>
            RateLimiter.CreateChained(
                new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 5,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(10),
                    TokensPerPeriod = 10,
                    AutoReplenishment = true
                }),
                new ConcurrencyLimiter(new ConcurrencyLimiterOptions
                {
                    PermitLimit = 5,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 2
                })));
    });
});

var app = builder.Build();

app.MapGet("/api/resource", () => "This endpoint uses multiple limiters")
   .RequireRateLimiting("combined");

요청은 계속 진행하려면 체인의 모든 리미터로부터 임대를 획득해야 하며, 제한기는 전달된 RateLimiter.CreateChained순서대로 실행됩니다. 제한자가 요청을 거부하면 요청이 거부되고 체인의 이전 리미터에서 이미 획득한 임대가 역순으로 삭제됩니다.

명명된 정책에서 제한기를 연결할 때 다음 사항에 유의하세요.

  • 임대를 삭제하면 동시성 제한에 대한 허가가 반환됩니다. 시간 기반 리미터(토큰 버킷, 고정 창 및 슬라이딩 윈도우)는 체인의 이후 리미터에서 요청을 거부할 때 이미 획득한 허가를 반환하지 않으므로 체인의 리미티더를 주문할 때 이를 고려합니다.
  • AutoReplenishmenttrue로 설정한 상태로 TokenBucketRateLimiter를 직접 생성하면 각 리미터 인스턴스가 자체 타이머를 갖게 됩니다. 대신 AddTokenBucketLimiterRateLimitPartition.GetTokenBucketLimiter 헬퍼는 AutoReplenishmentfalse로 설정하고, 단일 공유 타이머 하나로 각자의 모든 리미터를 보충합니다.
  • RateLimiter.CreateChained 는 전달된 리미터를 삭제하지 않습니다. 앞의 예제에서 파티션은 연결된 리미터를 캐시하고 프레임워크는 해당 수명을 관리합니다. 파티션 외부에서 연결된 리미터를 만드는 경우 더 이상 사용하지 않을 때 내부 리미터를 삭제합니다.
  • 체인이 모든 엔드포인트에 적용되어야 하는 경우 전역 PartitionedRateLimiter.CreateChained 접근 방식을 선호합니다. 체인의 범위를 특정 엔드포인트로 RateLimiter.CreateChained 지정해야 하는 경우에만 명명된 정책을 사용합니다.

요청 속도 제한 시 발생할 작업 선택

간단한 경우 상태 코드를 설정할 수 있습니다.

builder.Services.AddRateLimiter(options =>
{
    // Set a custom status code for rejections
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Rate limiter configuration...
});

가장 일반적인 방법은 속도 제한을 구성할 때 콜백을 OnRejected 등록하는 것입니다.

builder.Services.AddRateLimiter(options =>
{
    // Rate limiter configuration...

    options.OnRejected = async (context, cancellationToken) =>
    {
        // Custom rejection handling logic
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers["Retry-After"] = "60";

        await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);

        // Optional logging
        logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
            context.HttpContext.Connection.RemoteIpAddress);
    };
});

또 다른 옵션은 요청을 큐에 대기하는 것입니다.

요청 대기열 처리

큐를 사용하도록 설정하면 요청이 속도 제한을 초과하면 시스템에서 큐에 배치합니다. 요청은 허가가 사용 가능해지거나 시간 제한이 발생할 때까지 큐에서 대기합니다. 시스템은 구성 가능한 큐 순서에 따라 요청을 처리합니다.

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", options =>
    {
        options.PermitLimit = 10;           // Allow 10 requests
        options.Window = TimeSpan.FromSeconds(10);  // Per 10-second window
        options.QueueLimit = 5;             // Queue up to 5 additional requests
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Process oldest requests first
        options.AutoReplenishment = true; // Default: automatically replenish permits
    });
});

EnableRateLimitingDisableRateLimiting 특성

[EnableRateLimiting] 및 Razor 특성을 컨트롤러, 작업 메서드 또는 [DisableRateLimiting] 페이지에 적용합니다. Razor Pages의 경우 특성은 페이지 처리기가 아니라 Razor Page에 적용합니다. 예를 들어, [EnableRateLimiting], OnGet 또는 다른 페이지 핸들러에는 OnPost를 적용할 수 없습니다.

이 특성은 [DisableRateLimiting] 명명된 속도 제한기 또는 전역 제한에 관계없이 컨트롤러, 작업 방법 또는 Razor 페이지에 대한 속도 제한을 사용하지 않도록 설정합니다. 예를 들어 RequireRateLimiting을 호출하여 모든 컨트롤러 엔드포인트에 fixedPolicy 속도 제한을 적용하는 다음 코드를 살펴 봅시다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.SlidingPermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();
app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);

app.Run();

다음 코드에서 [DisableRateLimiting]은 속도 제한을 사용하지 않도록 설정하고 [EnableRateLimiting("fixed")]에서 호출된 Home2Controllerapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)에 적용된 Program.cs을 재정의합니다.

[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
    private readonly ILogger<Home2Controller> _logger;

    public Home2Controller(ILogger<Home2Controller> logger)
    {
        _logger = logger;
    }

    public ActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("sliding")]
    public ActionResult Privacy()
    {
        return View();
    }

    [DisableRateLimiting]
    public ActionResult NoLimit()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

앞의 코드에서 를 호출했기 때문에 적용되지 않습니다 작업 메서드에.

RequireRateLimiting 또는 MapRazorPages에서 MapDefaultControllerRoute을 호출하지 않는 다음 코드를 살펴봅시다.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.SlidingPermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages();
app.MapDefaultControllerRoute();  // RequireRateLimiting not called

app.Run();

다음 컨트롤러를 고려해보세요.

[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
    private readonly ILogger<Home2Controller> _logger;

    public Home2Controller(ILogger<Home2Controller> logger)
    {
        _logger = logger;
    }

    public ActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("sliding")]
    public ActionResult Privacy()
    {
        return View();
    }

    [DisableRateLimiting]
    public ActionResult NoLimit()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

이전 컨트롤러에서 다음을 수행합니다.

  • "fixed" 정책 속도 제한기는 EnableRateLimitingDisableRateLimiting 특성이 없는 모든 작업 메서드에 적용됩니다.
  • "sliding" 정책 속도 제한기는 Privacy 작업에 적용됩니다.
  • NoLimit 작업 메서드에서 속도 제한을 사용할 수 없습니다.

속도 제한 메트릭

속도 제한 미들웨어는 속도 제한이 앱 성능 및 사용자 환경에 미치는 영향을 이해하는 데 도움이 되는 기본 제공 메트릭 및 모니터링 기능을 제공합니다. 메트릭 목록은 다음을 참조하세요 Microsoft.AspNetCore.RateLimiting.

속도 제한을 사용하여 엔드포인트 테스트하기

프로덕션으로 속도 제한을 사용하는 앱을 배포하기 전에 앱을 스트레스 테스트하여 사용한 속도 제한 및 옵션의 유효성을 검사합니다. 예를 들어 BlazeMeter 또는 Apache JMeter HTTP(S) 테스트 스크립트 레코더와 같은 도구를 사용하여 JMeter 스크립트를 만들고 스크립트를 로드하여 Azure Load Testing.

사용자 입력을 사용하여 파티션을 만들면 앱이 DoS( 서비스 거부 ) 공격에 취약해집니다. 예를 들어 클라이언트 IP 주소를 사용하여 파티션을 만드는 경우 앱은 IP 원본 주소 스푸핑을 사용하는 서비스 거부 공격에 취약해집니다. 자세한 내용은 BCP 38 RFC 2827 네트워크 수신 필터링: IP 원본 주소 스푸핑을 사용하는 서비스 거부 공격을 참조하세요.

추가 리소스