ASP.NET Core'da hız sınırlama ara yazılımı

Arvin Kahbazi, Maarten Balliauw ve Rick Anderson tarafından

Ara Microsoft.AspNetCore.RateLimiting yazılım, hız sınırlama ara yazılımı sağlar. Uygulamalar hız sınırlama ilkelerini yapılandırıp ilkeleri uç noktalara ekler. Hız sınırlama kullanan uygulamalar, dağıtmadan önce dikkatle yüklenmelidir ve gözden geçirilmelidir. Daha fazla bilgi için bu makaledeki Hız sınırlaması ile uç noktaları test etme konusuna bakın.

Hız sınırlamaya giriş için bkz . Hız sınırlama ara yazılımı.

Hız sınırlayıcı algoritmaları

sınıfı hız RateLimiterOptionsExtensions sınırlama için aşağıdaki uzantı yöntemlerini sağlar:

Sabit pencere sınırlayıcısı

yöntemi, AddFixedWindowLimiter istekleri sınırlamak için sabit bir zaman penceresi kullanır. Zaman penceresinin süresi dolduğunda yeni bir zaman penceresi başlatılır ve istek sınırı sıfırlanır.

Aşağıdaki kodu inceleyin:

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();

Yukarıdaki kod:

  • Hizmet koleksiyonuna hız sınırlama hizmeti eklemek için çağrılar AddRateLimiter .
  • İlke adı "fixed" ve olan sabit bir pencere sınırlayıcısı oluşturmak için çağrılarAddFixedWindowLimiter:
  • PermitLimit 4'e ve 12'ye kadar olan süre Window . Her 12 saniyelik pencere başına en fazla 4 isteğe izin verilir.
  • QueueProcessingOrder için OldestFirst.
  • QueueLimit 2'ye kadar.
  • Hız sınırlamayı etkinleştirmek için UseRateLimiter'ı çağırır.

Uygulamalar sınırlayıcı seçenekleri ayarlamak için Yapılandırma'yı kullanmalıdır. Aşağıdaki kod, yapılandırma için kullanarak MyRateLimitOptions önceki kodu güncelleştirir:

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();

UseRateLimiter hız sınırlama uç noktasına özgü API'ler kullanıldıktan sonra UseRouting çağrılmalıdır. Örneğin, özniteliği kullanılıyorsa [EnableRateLimiting] , UseRateLimiter sonra UseRoutingçağrılmalıdır. Yalnızca genel sınırlayıcılar çağrılırken, UseRateLimiter öncesinde UseRoutingçağrılabilir.

Kayan pencere sınırlayıcısı

Kayan pencere algoritması:

  • Sabit pencere sınırlayıcısına benzer ancak pencere başına segmentler ekler. Pencere, her segment aralığı için bir segment kaydırıyor. Segment aralığı (pencere zamanı)/(pencere başına segmentler) aralığıdır.
  • Pencere permitLimit isteklerini isteklerle sınırlar.
  • Her zaman penceresi pencere başına segmentlere bölünür n .
  • Süresi dolan zaman segmentinden bir pencere geri alınan istekler (n geçerli segmentten önceki segmentler) geçerli segmente eklenir. Son kullanma tarihi geçmiş segment olarak bir pencere geri dönerek en çok süresi dolmuş zaman segmentine başvuruyoruz.

30 saniyelik pencere, pencere başına üç segment ve 100 istek sınırı içeren kayan pencere sınırlayıcısını gösteren aşağıdaki tabloyu göz önünde bulundurun:

  • Üst satır ve ilk sütunda zaman kesimi gösterilir.
  • İkinci satır, kullanılabilir kalan istekleri gösterir. Kalan istekler, kullanılabilir istekler eksi işlenen istekler ve geri dönüştürülen istekler olarak hesaplanır.
  • İstekler her seferinde çapraz mavi çizgi boyunca hareket eder.
  • 30'dan itibaren süresi dolan zaman kesiminden alınan istek, kırmızı çizgilerde gösterildiği gibi istek sınırına geri eklenir.

İstekleri, sınırları ve geri dönüştürülen yuvaları gösteren tablo

Aşağıdaki tabloda, önceki grafikteki veriler farklı bir biçimde gösterilmiştir. Kullanılabilir sütunu, önceki segmentten (Önceki satırdan taşıma) sağlanan istekleri gösterir. İlk satırda 100 kullanılabilir istek gösterilir çünkü önceki bir segment yoktur.

Saat Kullanılabilir Alınan Süresi dolan geri dönüştürüldü Taşıma
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

Aşağıdaki kod kayan pencere hızı sınırlayıcısını kullanır:

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();

Belirteç demet sınırlayıcısı

Belirteç demet sınırlayıcısı kayan pencere sınırlayıcısına benzer, ancak süresi dolan segmentten alınan istekleri geri eklemek yerine, her yenileme dönemi için sabit sayıda belirteç eklenir. Her segmente eklenen belirteçler, kullanılabilir belirteçleri belirteç demet sınırından daha yüksek bir sayıya yükseltemez. Aşağıdaki tabloda, 100 belirteç sınırı ve 10 saniyelik yenileme süresi olan bir belirteç demet sınırlayıcısı gösterilmektedir.

Saat Kullanılabilir Alınan Eklendi Taşıma
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

Aşağıdaki kod belirteç demet sınırlayıcısını kullanır:

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();

AutoReplenishment olarak ayarlandığındatrue, iç zamanlayıcı belirteçleri her ReplenishmentPeriodbiri olarak yeniler; olarak ayarlandığındafalse, uygulamanın sınırlayıcıda çağrısı TryReplenish yapması gerekir.

Eşzamanlılık sınırlayıcısı

Eşzamanlılık sınırlayıcısı eşzamanlı istek sayısını sınırlar. Her istek eşzamanlılık sınırını bir azaltır. İstek tamamlandığında, sınır bir artırılır. Belirli bir süre için toplam istek sayısını sınırlayan diğer istek sınırlayıcılarından farklı olarak, eşzamanlılık sınırlayıcısı yalnızca eşzamanlı istek sayısını sınırlar ve bir zaman aralığındaki istek sayısını sınırlamaz.

Aşağıdaki kod eşzamanlılık sınırlayıcısını kullanır:

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();

Zincirlenmiş sınırlayıcılar oluşturma

API, CreateChained tek bir PartitionedRateLimiteriçine birleştirilen birden çok PartitionedRateLimiter öğesinin geçirilmesine izin verir. Birleşik sınırlayıcı, tüm giriş sınırlayıcılarını sırayla çalıştırır.

Aşağıdaki kodda kullanılır CreateChained:

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
    _.OnRejected = (context, _) =>
    {
        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;
        context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");

        return new ValueTask();
    };
    _.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();

Daha fazla bilgi için bkz . CreateChained kaynak kodu

EnableRateLimiting ve DisableRateLimiting öznitelikleri

[EnableRateLimiting] ve [DisableRateLimiting] öznitelikleri bir Denetleyiciye, eylem yöntemine veya Razor Sayfaya uygulanabilir. Sayfalar için Razor özniteliği sayfa işleyicilerine Razor değil Sayfaya uygulanmalıdır. Örneğin, [EnableRateLimiting] , OnPostveya başka bir sayfa işleyicisine OnGetuygulanamaz.

özniteliği, [DisableRateLimiting] adlandırılmış hız sınırlayıcılarına veya genel sınırlayıcılara bakılmaksızın Denetleyici, eylem yöntemi veya Razor Sayfa ile hız sınırlamayı devre dışı bırakır. Örneğin, hız sınırlamasını tüm denetleyici uç noktalarına uygulamak fixedPolicy için çağıran RequireRateLimiting aşağıdaki kodu göz önünde bulundurun:

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();

Aşağıdaki kodda, [DisableRateLimiting] hız sınırlamasını devre dışı bırakır ve içinde Program.csolarak adlandırılan ve app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) öğesine Home2Controller uygulanan geçersiz kılmaları [EnableRateLimiting("fixed")] devre dışı bırakır:

[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 });
    }
}

Yukarıdaki kodda, [EnableRateLimiting("sliding")] adlı app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)eylem yöntemine Program.cs Privacy uygulanmaz.

veya MapDefaultControllerRouteüzerinde MapRazorPages çağrılmayıp RequireRateLimiting aşağıdaki kodu göz önünde bulundurun:

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();

Aşağıdaki denetleyiciyi göz önünde bulundurun:

[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 });
    }
}

Önceki denetleyicide:

  • İlke "fixed" hızı sınırlayıcısı, ve DisableRateLimiting öznitelikleri olmayan EnableRateLimiting tüm eylem yöntemlerine uygulanır.
  • İlke "sliding" hızı sınırlayıcısı eyleme Privacy uygulanır.
  • Eylem yönteminde NoLimit hız sınırlama devre dışı bırakıldı.

Sayfalar'a Razor öznitelik uygulama

Sayfalar için Razor özniteliği sayfa işleyicilerine Razor değil Sayfaya uygulanmalıdır. Örneğin, [EnableRateLimiting] , OnPostveya başka bir sayfa işleyicisine OnGetuygulanamaz.

özniteliği, DisableRateLimiting bir Razor Sayfada hız sınırlamayı devre dışı bırakır. EnableRateLimitingyalnızca çağrılmadıysa sayfaya Razor MapRazorPages().RequireRateLimiting(Policy)uygulanır.

Sınırlayıcı algoritması karşılaştırması

Sabit, kayan ve belirteç sınırlayıcılarının tümü, bir zaman aralığındaki en fazla istek sayısını sınırlar. Eşzamanlılık sınırlayıcısı yalnızca eşzamanlı istek sayısını sınırlar ve bir zaman aralığındaki istek sayısını sınırlamaz. Sınırlayıcı seçilirken uç noktanın maliyeti dikkate alınmalıdır. Bir uç noktanın maliyeti, zaman, veri erişimi, CPU ve G/Ç gibi kullanılan kaynakları içerir.

Hız sınırlayıcı örnekleri

Aşağıdaki örnekler üretim kodu için tasarlanmamıştır ancak sınırlayıcıların nasıl kullanılacağına ilişkin örneklerdir.

, RetryAfterve ile OnRejectedsınırlayıcıGlobalLimiter

Aşağıdaki örnek:

  • İstek belirtilen sınırı aştığında çağrılan bir RateLimiterOptions.OnRejected geri çağırması oluşturur. retryAfter, FixedWindowLimiterve SlidingWindowLimiter ile TokenBucketRateLimiterkullanılabilir çünkü bu algoritmalar ne zaman daha fazla izin ekleneceğini tahmin edebilir. izinlerin ConcurrencyLimiter ne zaman kullanılabilir olacağını hesaplamanın hiçbir yolu yoktur.

  • Aşağıdaki sınırlayıcıları ekler:

    • SampleRateLimiterPolicy Arabirimi uygulayan IRateLimiterPolicy<TPartitionKey> bir. Sınıfı SampleRateLimiterPolicy bu makalenin ilerleyen bölümlerinde gösterilir.
    • A SlidingWindowLimiter:
      • Kimliği doğrulanmış her kullanıcı için bir bölüm ile.
      • Tüm anonim kullanıcılar için bir paylaşılan bölüm.
    • GlobalLimiter Tüm isteklere uygulanan bir. Genel sınırlayıcı önce yürütülür, ardından varsa uç noktaya özgü sınırlayıcı yürütülür. , GlobalLimiter her IPAddressiçin bir bölüm oluşturur.
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
    throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

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

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

var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(limiterOptions =>
{
    limiterOptions.OnRejected = (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;
        context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
            .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
            .LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));

        return new ValueTask();
    };

    limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
    limiterOptions.AddPolicy(userPolicyName, context =>
    {
        var username = "anonymous user";
        if (context.User.Identity?.IsAuthenticated is true)
        {
            username = context.User.ToString()!;
        }

        return RateLimitPartition.GetSlidingWindowLimiter(username,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = myOptions.PermitLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                Window = TimeSpan.FromSeconds(myOptions.Window),
                SegmentsPerWindow = myOptions.SegmentsPerWindow
            });

    });
    
    limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
    {
        IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;

        if (!IPAddress.IsLoopback(remoteIpAddress!))
        {
            return RateLimitPartition.GetTokenBucketLimiter
            (remoteIpAddress!, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
    });
});

var app = builder.Build();

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

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

app.UseRouting();
app.UseRateLimiter();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();

static string GetUserEndPoint(HttpContext context) =>
   $"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
   + $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(userPolicyName);

app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(helloPolicy);

app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");

app.Run();

Uyarı

İstemci IP adreslerinde bölümler oluşturmak, uygulamayı IP Kaynak Adresi Kimlik Sahtekarlığı kullanan Hizmet Reddi Saldırılarına karşı savunmasız hale getirir. Daha fazla bilgi için bkz . BCP 38 RFC 2827 Ağ Girişi Filtreleme: IP Kaynak Adresi Kimlik Sahtekarlığına Neden Olan Hizmet Reddi Saldırılarını Önleme.

Dosyanın tamamı Program.cs için örnek deposuna bakın.

Sınıfı SampleRateLimiterPolicy

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

namespace WebRateLimitAuth;

public class SampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
    private Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected;
    private readonly MyRateLimitOptions _options;

    public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger,
                                   IOptions<MyRateLimitOptions> options)
    {
        _onRejected = (ctx, token) =>
        {
            ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}");
            return ValueTask.CompletedTask;
        };
        _options = options.Value;
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected;

    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        return RateLimitPartition.GetSlidingWindowLimiter(string.Empty,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = _options.PermitLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = _options.QueueLimit,
                Window = TimeSpan.FromSeconds(_options.Window),
                SegmentsPerWindow = _options.SegmentsPerWindow
            });
    }
}

Yukarıdaki kodda, OnRejected yanıt durumunu 429 Çok Fazla İstek olarak ayarlamak için kullanırOnRejectedContext. Varsayılan reddedilen durum 503 Hizmet Kullanılamıyor şeklindedir.

Yetkilendirmeli sınırlayıcı

Aşağıdaki örnek JSON Web Belirteçlerini (JWT) kullanır ve JWT erişim belirteci ile bir bölüm oluşturur. Bir üretim uygulamasında JWT genellikle Güvenlik belirteci hizmeti (STS) olarak davranan bir sunucu tarafından sağlanabilir. Yerel geliştirme için, uygulamaya özgü yerel JWT'ler oluşturmak ve yönetmek için dotnet user-jwts komut satırı aracı kullanılabilir.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var jwtPolicyName = "jwt";

builder.Services.AddRateLimiter(limiterOptions =>
{
    limiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    limiterOptions.AddPolicy(policyName: jwtPolicyName, partitioner: httpContext =>
    {
        var accessToken = httpContext.Features.Get<IAuthenticateResultFeature>()?
                              .AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
                          ?? string.Empty;

        if (!StringValues.IsNullOrEmpty(accessToken))
        {
            return RateLimitPartition.GetTokenBucketLimiter(accessToken, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = myOptions.TokenLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                TokensPerPeriod = myOptions.TokensPerPeriod,
                AutoReplenishment = true
            });
    });
});

var app = builder.Build();

app.UseAuthorization();
app.UseRateLimiter();

app.MapGet("/", () => "Hello, World!");

app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
    .RequireRateLimiting(jwtPolicyName)
    .RequireAuthorization();

app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
    .RequireRateLimiting(jwtPolicyName)
    .RequireAuthorization();

app.Run();

static string GetUserEndPointMethod(HttpContext context) =>
    $"Hello {context.User.Identity?.Name ?? "Anonymous"} " +
    $"Endpoint:{context.Request.Path} Method: {context.Request.Method}";

, TokenBucketRateLimiterve yetkilendirme ile ConcurrencyLimitersınırlayıcı

Aşağıdaki örnek:

  • Sayfalar'da Razor kullanılan ilke adına "get" sahip bir ConcurrencyLimiter ekler.
  • TokenBucketRateLimiter Her yetkili kullanıcı için bir bölüm ve tüm anonim kullanıcılar için bir bölüm ekler.
  • RateLimiterOptions.RejectionStatusCode'ı 429 Çok Fazla İstek olarak ayarlar.
var getPolicyName = "get";
var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddConcurrencyLimiter(policyName: getPolicyName, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    })
    .AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
    {
        string userName = httpContext.User.Identity?.Name ?? string.Empty;

        if (!StringValues.IsNullOrEmpty(userName))
        {
            return RateLimitPartition.GetTokenBucketLimiter(userName, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = myOptions.TokenLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                TokensPerPeriod = myOptions.TokensPerPeriod,
                AutoReplenishment = true
            });
    }));

Dosyanın tamamı Program.cs için örnek deposuna bakın.

Hız sınırlaması ile uç noktaları test etme

Üretimde hız sınırlaması kullanarak bir uygulamayı dağıtmadan önce, kullanılan hız sınırlayıcıları ve seçenekleri doğrulamak için uygulamayı stres testi yapın. Örneğin, BlazeMeter veya Apache JMeter HTTP(S) Test Betiği Kaydedicisi gibi bir araçla bir JMeter betiği oluşturun ve betiği Azure Load Testing'e yükleyin.

Kullanıcı girişiyle bölümler oluşturmak, uygulamayı Hizmet Reddi (DoS) Saldırılarına karşı savunmasız hale getirir. Örneğin, istemci IP adreslerinde bölümler oluşturmak, uygulamayı IP Kaynak Adresi Kimlik Sahtekarlığı kullanan Hizmet Reddi Saldırılarına karşı savunmasız hale getirir. Daha fazla bilgi için bkz . BCP 38 RFC 2827 Ağ Girişi Filtreleme: IP Kaynak Adresi Kimlik Sahtekarlığına Neden Olan Hizmet Reddi Saldırılarını Önleme.

Ek kaynaklar