Bagikan melalui


Middleware pembatasan laju di ASP.NET Core

Oleh Arvin Kahbazi, Maarten Balliauw, dan Rick Anderson

Middleware Microsoft.AspNetCore.RateLimiting menyediakan middleware pembatasan laju. Aplikasi mengonfigurasi kebijakan pembatasan tarif lalu melampirkan kebijakan ke titik akhir. Aplikasi yang menggunakan pembatasan tarif harus diuji dan ditinjau dengan cermat sebelum disebarkan. Lihat Menguji titik akhir dengan pembatasan tarif dalam artikel ini untuk informasi selengkapnya.

Untuk pengenalan pembatasan tarif, lihat Middleware pembatasan laju.

Algoritma pembatas laju

Kelas RateLimiterOptionsExtensions ini menyediakan metode ekstensi berikut untuk pembatasan tarif:

Pembatas jendela tetap

Metode ini AddFixedWindowLimiter menggunakan jendela waktu tetap untuk membatasi permintaan. Saat jendela waktu kedaluwarsa, jendela waktu baru dimulai dan batas permintaan diatur ulang.

Pertimbangkan gambar berikut:

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

Kode sebelumnya:

Aplikasi harus menggunakan Konfigurasi untuk mengatur opsi pembatas. Kode berikut memperbarui kode sebelumnya menggunakan MyRateLimitOptions untuk konfigurasi:

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 harus dipanggil setelah UseRouting ketika pembatasan laju API tertentu titik akhir digunakan. Misalnya, jika [EnableRateLimiting] atribut digunakan, UseRateLimiter harus dipanggil setelah UseRouting. Saat hanya memanggil pembatas global, UseRateLimiter dapat dipanggil sebelum UseRouting.

Pembatas jendela geser

Algoritma jendela geser:

  • Mirip dengan pembatas jendela tetap tetapi menambahkan segmen per jendela. Jendela menggeser satu segmen setiap interval segmen. Interval segmen adalah (window time)/(segmen per jendela).
  • Membatasi permintaan untuk jendela ke permitLimit permintaan.
  • Setiap jendela waktu dibagi dalam n segmen per jendela.
  • Permintaan yang diambil dari segmen waktu kedaluwarsa satu jendela kembali (n segmen sebelum segmen saat ini) ditambahkan ke segmen saat ini. Kami merujuk ke segmen waktu yang paling kedaluwarsa satu jendela kembali sebagai segmen yang kedaluwarsa.

Pertimbangkan tabel berikut yang memperlihatkan pembatas jendela geser dengan jendela 30 detik, tiga segmen per jendela, dan batas 100 permintaan:

  • Baris atas dan kolom pertama memperlihatkan segmen waktu.
  • Baris kedua memperlihatkan permintaan yang tersisa yang tersedia. Permintaan yang tersisa dihitung sebagai permintaan yang tersedia dikurangi permintaan yang diproses ditambah permintaan daur ulang.
  • Permintaan setiap kali bergerak di sepanjang garis biru diagonal.
  • Dari waktu 30, permintaan yang diambil dari segmen waktu kedaluwarsa ditambahkan kembali ke batas permintaan, seperti yang ditunjukkan pada garis merah.

Tabel memperlihatkan permintaan, batas, dan slot daur ulang

Tabel berikut ini memperlihatkan data dalam grafik sebelumnya dalam format yang berbeda. Kolom Yang tersedia memperlihatkan permintaan yang tersedia dari segmen sebelumnya (Carry over dari baris sebelumnya). Baris pertama menunjukkan 100 permintaan yang tersedia karena tidak ada segmen sebelumnya.

Waktu Tersedia Diambil Didaur ulang dari kedaluwarsa Membawa ke atas
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

Kode berikut menggunakan pembatas laju jendela geser:

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

Pembatas wadah token

Pembatas wadah token mirip dengan pembatas jendela geser, tetapi daripada menambahkan kembali permintaan yang diambil dari segmen yang kedaluwarsa, jumlah token tetap ditambahkan setiap periode pengisian ulang. Token yang ditambahkan setiap segmen tidak dapat meningkatkan token yang tersedia ke angka yang lebih tinggi dari batas wadah token. Tabel berikut menunjukkan pembatas wadah token dengan batas 100 token dan periode pengisian ulang 10 detik.

Waktu Tersedia Diambil Ditambahkan Membawa ke atas
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

Kode berikut menggunakan pembatas wadah token:

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

Ketika AutoReplenishment diatur ke true, timer internal mengisi ulang token setiap ReplenishmentPeriod; ketika diatur ke false, aplikasi harus memanggil TryReplenish pembatas.

Pembatas konkurensi

Pembatas konkurensi membatasi jumlah permintaan bersamaan. Setiap permintaan mengurangi batas konkurensi satu per satu. Ketika permintaan selesai, batas ditingkatkan satu. Tidak seperti pembatas permintaan lain yang membatasi jumlah total permintaan untuk periode tertentu, pembatas konkurensi hanya membatasi jumlah permintaan bersamaan dan tidak membatasi jumlah permintaan dalam jangka waktu tertentu.

Kode berikut menggunakan pembatas konkurensi:

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

Membuat pembatas berantai

CreateChained API memungkinkan passing dalam beberapa PartitionedRateLimiter yang digabungkan menjadi satu PartitionedRateLimiter. Pembatas gabungan menjalankan semua pembatas input secara berurutan.

Kode berikut menggunakan 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();

Untuk informasi selengkapnya, lihat kode sumber CreateChained

EnableRateLimiting dan atribut DisableRateLimiting

Atribut [EnableRateLimiting] dan [DisableRateLimiting] dapat diterapkan ke Pengontrol, metode tindakan, atau Razor Halaman. Untuk Razor Halaman, atribut harus diterapkan ke Razor Halaman dan bukan penangan halaman. Misalnya, [EnableRateLimiting] tidak dapat diterapkan ke OnGet, , OnPostatau handler halaman lainnya.

Atribut [DisableRateLimiting] menonaktifkan pembatasan laju ke Pengontrol, metode tindakan, atau Razor Halaman terlepas dari pembatas tarif bernama atau pembatas global yang diterapkan. Misalnya, pertimbangkan kode berikut yang memanggil RequireRateLimiting untuk menerapkan fixedPolicy pembatasan tarif ke semua titik akhir pengontrol:

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

Dalam kode berikut, [DisableRateLimiting] menonaktifkan pembatasan tarif dan penimpaan [EnableRateLimiting("fixed")] yang diterapkan ke dan app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) dipanggil Home2Controller dalam 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 });
    }
}

Dalam kode sebelumnya, [EnableRateLimiting("sliding")] tidak diterapkan ke Privacy metode tindakan karena Program.cs disebut .app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)

Pertimbangkan kode berikut yang tidak memanggil RequireRateLimiting MapRazorPages atau 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();

Pertimbangkan pengontrol berikut:

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

Di pengontrol sebelumnya:

  • Pembatas "fixed" tarif kebijakan diterapkan ke semua metode tindakan yang tidak memiliki EnableRateLimiting atribut dan DisableRateLimiting .
  • Pembatas "sliding" tarif kebijakan diterapkan pada Privacy tindakan.
  • Pembatasan tarif dinonaktifkan pada NoLimit metode tindakan.

Menerapkan atribut ke Razor Pages

Untuk Razor Halaman, atribut harus diterapkan ke Razor Halaman dan bukan penangan halaman. Misalnya, [EnableRateLimiting] tidak dapat diterapkan ke OnGet, , OnPostatau handler halaman lainnya.

Atribut DisableRateLimiting menonaktifkan pembatasan tarif pada Razor Halaman. EnableRateLimitinghanya diterapkan ke Razor Halaman jika MapRazorPages().RequireRateLimiting(Policy) belum dipanggil.

Perbandingan algoritma pembatas

Pembatas tetap, geser, dan token semuanya membatasi jumlah maksimum permintaan dalam periode waktu tertentu. Pembatas konkurensi hanya membatasi jumlah permintaan bersamaan dan tidak membatasi jumlah permintaan dalam jangka waktu tertentu. Biaya titik akhir harus dipertimbangkan saat memilih pembatas. Biaya titik akhir mencakup sumber daya yang digunakan, misalnya, waktu, akses data, CPU, dan I/O.

Sampel pembatas laju

Sampel berikut tidak dimaksudkan untuk kode produksi tetapi merupakan contoh tentang cara menggunakan pembatas.

Pembatas dengan OnRejected, RetryAfter, dan GlobalLimiter

Contoh berikut:

  • Membuat panggilan balik RateLimiterOptions.OnRejected yang dipanggil saat permintaan melebihi batas yang ditentukan. retryAfter dapat digunakan dengan TokenBucketRateLimiter, FixedWindowLimiter, dan SlidingWindowLimiter karena algoritma ini dapat memperkirakan kapan lebih banyak izin akan ditambahkan. ConcurrencyLimiter tidak memiliki cara menghitung kapan izin akan tersedia.

  • Menambahkan pembatas berikut:

    • Yang SampleRateLimiterPolicy mengimplementasikan IRateLimiterPolicy<TPartitionKey> antarmuka. Kelas SampleRateLimiterPolicy ditampilkan nanti dalam artikel ini.
    • A SlidingWindowLimiter:
      • Dengan partisi untuk setiap pengguna yang diautentikasi.
      • Satu partisi bersama untuk semua pengguna anonim.
    • GlobalLimiter Yang diterapkan ke semua permintaan. Pembatas global akan dijalankan terlebih dahulu, diikuti oleh pembatas khusus titik akhir, jika ada. membuat GlobalLimiter partisi untuk setiap IPAddress.
// 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();

Peringatan

Membuat partisi pada alamat IP klien membuat aplikasi rentan terhadap Penolakan Serangan Layanan yang menggunakan Spoofing Alamat Sumber IP. Untuk informasi selengkapnya, lihat Pemfilteran Ingress Jaringan BCP 38 RFC 2827: Mengalahkan Penolakan Serangan Layanan yang menggunakan Spoofing Alamat Sumber IP.

Lihat repositori sampel untuk file lengkap Program.cs .

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

Dalam kode sebelumnya, OnRejected menggunakan OnRejectedContext untuk mengatur status respons ke 429 Terlalu Banyak Permintaan. Status default yang ditolak adalah 503 Layanan Tidak Tersedia.

Pembatas dengan otorisasi

Sampel berikut menggunakan JSON Web Tokens (JWT) dan membuat partisi dengan token akses JWT. Dalam aplikasi produksi, JWT biasanya akan disediakan oleh server yang bertindak sebagai layanan token Keamanan (STS). Untuk pengembangan lokal, alat baris perintah dotnet user-jwts dapat digunakan untuk membuat dan mengelola JWT lokal khusus aplikasi.

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}";

Pembatas dengan ConcurrencyLimiter, TokenBucketRateLimiter, dan otorisasi

Contoh berikut:

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

Lihat repositori sampel untuk file lengkap Program.cs .

Menguji titik akhir dengan pembatasan tarif

Sebelum menyebarkan aplikasi menggunakan pembatasan laju untuk produksi, uji stres aplikasi untuk memvalidasi pembatas tarif dan opsi yang digunakan. Misalnya, buat skrip JMeter dengan alat seperti BlazeMeter atau Apache JMeter HTTP(S) Test Script Recorder dan muat skrip ke Azure Load Testing.

Membuat partisi dengan input pengguna membuat aplikasi rentan terhadap Serangan Denial of Service (DoS). Misalnya, membuat partisi pada alamat IP klien membuat aplikasi rentan terhadap Penolakan Serangan Layanan yang menggunakan Spoofing Alamat Sumber IP. Untuk informasi selengkapnya, lihat Pemfilteran Ingress Jaringan BCP 38 RFC 2827: Mengalahkan Penolakan Serangan Layanan yang menggunakan Spoofing Alamat Sumber IP.

Sumber Daya Tambahan: