Middleware batas laju di ASP.NET Core

Oleh Arvin Kahbazi, Maarten Balliauw, dan Rick Anderson

Middleware Microsoft.AspNetCore.RateLimiting menyediakan middleware untuk 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 endpoint dengan pembatasan kecepatan dalam artikel ini untuk informasi lebih lanjut.

Untuk pengenalan pembatasan laju, lihat Middleware pembatasan laju.

Mengapa menggunakan pembatasan tarif

Pembatasan tarif dapat digunakan untuk mengelola alur permintaan masuk ke aplikasi. Alasan utama untuk menerapkan pembatasan tarif:

  • Mencegah Penyalahgunaan: Pembatasan tarif membantu melindungi aplikasi dari penyalahgunaan dengan membatasi jumlah permintaan yang dapat dibuat pengguna atau klien dalam periode waktu tertentu. Perlindungan ini sangat penting untuk API publik.
  • Memastikan Penggunaan yang Adil: Dengan menetapkan batasan yang mencegah pengguna memonopoli sistem, Anda memastikan bahwa semua pengguna memiliki akses yang adil ke sumber daya.
  • Melindungi Sumber Daya: Pembatasan tarif membantu mencegah kelebihan beban server dengan mengontrol jumlah permintaan yang dapat diproses. Ini melindungi sumber daya backend agar tidak kewalahan.
  • Meningkatkan Keamanan: Ini dapat mengurangi risiko serangan Denial of Service (DoS) dengan membatasi tingkat di mana permintaan diproses. Itu membuatnya lebih sulit bagi penyerang untuk membanjiri sistem.
  • Meningkatkan Performa: Dengan mengontrol tingkat permintaan masuk, Anda dapat mempertahankan performa dan responsivitas aplikasi yang optimal, memastikan pengalaman pengguna yang lebih baik.
  • Cost Management: Untuk layanan yang dikenakan biaya berdasarkan penggunaan, pembatasan tarif dapat membantu mengelola dan memprediksi pengeluaran dengan mengontrol volume permintaan yang diproses.

Menerapkan pembatasan tarif dalam aplikasi ASP.NET Core dapat membantu menjaga stabilitas, keamanan, dan performa. Hasilnya adalah layanan yang andal dan efisien untuk semua pengguna.

Mencegah serangan DDoS

Meskipun pembatasan tarif dapat membantu mengurangi risiko serangan Denial of Service (DoS) dengan membatasi tingkat di mana permintaan diproses, ini bukan solusi komprehensif untuk serangan Distributed Denial of Service (DDoS). Serangan DDoS melibatkan beberapa sistem yang membanjiri aplikasi dengan banjir permintaan, sehingga sulit ditangani dengan pembatasan laju saja.

Untuk perlindungan DDoS yang kuat, pertimbangkan untuk menggunakan layanan perlindungan DDoS komersial. Layanan ini menawarkan fitur lanjutan seperti:

  • Analisis lalu lintas: Pemantauan dan analisis berkelanjutan lalu lintas masuk untuk mendeteksi dan mengurangi serangan DDoS secara real time.
  • Skalabilitas: Kemampuan untuk menangani serangan skala besar dengan mendistribusikan lalu lintas di beberapa server dan pusat data.
  • Mitigasi otomatis: Mekanisme respons otomatis untuk memblokir lalu lintas berbahaya dengan cepat tanpa intervensi manual.
  • Jaringan global: Jaringan global server untuk menyerap dan mengurangi serangan lebih dekat ke sumbernya.
  • Pembaruan konstan: Layanan komersial terus melacak dan memperbarui mekanisme perlindungan mereka untuk beradaptasi dengan ancaman baru dan berkembang.

Saat menggunakan layanan hosting cloud, perlindungan DDoS biasanya tersedia sebagai bagian dari solusi hosting, seperti Azure Web Application Firewall, AWS Shield atau Google Cloud Armor. Perlindungan khusus tersedia sebagai Web Application Firewalls (WAF) atau sebagai bagian dari solusi CDN seperti Cloudflare atau Akamai Kona Site Defender

Menerapkan layanan perlindungan DDoS komersial bersama dengan pembatasan tarif dapat memberikan strategi pertahanan yang komprehensif, memastikan stabilitas, keamanan, dan performa aplikasi.

Gunakan middleware pembatasan laju

Langkah-langkah berikut menunjukkan cara menggunakan middleware pembatasan laju di aplikasi ASP.NET Core:

  1. Mengonfigurasi layanan pembatasan tarif.

Dalam file Program.cs, konfigurasikan layanan pembatasan tarif dengan menambahkan kebijakan pembatasan tarif yang sesuai. Tentukan kebijakan sebagai kebijakan global maupun kebijakan bernama. Contoh berikut mengizinkan 10 permintaan per menit berdasarkan pengguna (identitas) atau secara global:

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

Kebijakan bernama perlu diterapkan secara eksplisit ke halaman atau titik akhir. Contoh berikut menambahkan kebijakan pembatas jendela tetap bernama "fixed" yang Anda tambahkan ke titik akhir nanti:

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

Pembatas global berlaku untuk semua titik akhir secara otomatis saat Anda mengonfigurasinya melalui opsi. GlobalLimiter.

  1. Aktifkan middleware pembatasan kecepatan

    Dalam file Program.cs, aktifkan middleware pembatasan tarif dengan memanggil UseRateLimiter:

app.UseRouting();

app.UseRateLimiter();

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

app.Run();

Menerapkan kebijakan pembatasan tarif ke titik akhir atau halaman

Menerapkan pembatasan laju ke titik akhir API Web

Terapkan kebijakan bernama ke titik akhir atau grup, misalnya:


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

Menerapkan pembatasan tarif ke pengontrol MVC

Terapkan kebijakan pembatasan tarif yang dikonfigurasi ke titik akhir tertentu atau secara global. Misalnya, untuk menerapkan kebijakan "tetap" ke semua titik akhir pengontrol:

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

Menerapkan pembatasan tarif ke aplikasi Blazor sisi server

Untuk menetapkan pembatasan tarif untuk semua komponen Razor yang dapat dirutekan aplikasi, tentukan RequireRateLimiting dengan nama kebijakan pembatasan tarif pada panggilan MapRazorComponents dalam file Program. Dalam contoh berikut, kebijakan pembatasan tarif bernama "policy" diterapkan:

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

Untuk menetapkan kebijakan untuk satu komponen yang dapat dirutekan Razor atau folder komponen melalui file impor (_Imports.razor), terapkan [EnableRateLimiting] atribut dengan nama kebijakan. Dalam contoh berikut, kebijakan pembatasan tarif bernama "override" diterapkan. Kebijakan ini menggantikan semua kebijakan yang saat ini diterapkan pada endpoint. Pembatas global tetap berfungsi pada titik akhir dengan penerapan atribut ini.

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

<h1>Counter</h1>

[EnableRateLimiting] Terapkan atribut hanya ke komponen yang dapat dirutekan atau folder komponen melalui file impor jika RequireRateLimitingtidak dipanggil pada MapRazorComponents.

[DisableRateLimiting] Gunakan atribut untuk menonaktifkan pembatasan laju untuk komponen yang dapat dirutekan atau folder komponen melalui file impor.

Algoritma pembatas laju

Kelas RateLimiterOptionsExtensions ini menyediakan metode ekstensi berikut untuk pembatasan tarif:

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. Pertimbangkan biaya titik akhir saat Anda memilih pembatas. Biaya titik akhir mencakup sumber daya yang digunakan, seperti waktu, akses data, CPU, dan I/O.

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

Pengatur jendela geser

Algoritma dengan jendela geser

  • Mirip dengan pembatas jendela tetap tetapi menambahkan segmen per jendela. Jendela menggeser satu segmen setiap selang segmen. Interval segmen adalah (waktu jendela)/(segmen per jendela).
  • Membatasi jumlah permintaan jendela ke permitLimit permintaan.
  • Setiap jendela waktu dibagi dalam n segmen per jendela.
  • Permintaan yang diambil dari segmen waktu yang sudah kedaluwarsa pada satu jendela sebelumnya (n segmen sebelum segmen saat ini) ditambahkan ke segmen saat ini. Kami merujuk ke segmen waktu yang paling kedaluwarsa dari satu jendela sebelumnya 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 masih tersedia. Permintaan yang tersisa dihitung sebagai permintaan yang tersedia dikurangi permintaan yang diproses ditambah permintaan daur ulang.
  • Permintaan pada setiap waktu bergerak di sepanjang garis biru diagonal.
  • Mulai dari waktu 30, permintaan dari segmen waktu yang sudah kedaluwarsa ditambahkan kembali ke dalam 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 Tersedia memperlihatkan permintaan yang tersedia dari segmen sebelumnya (Dibawa dari baris sebelumnya). Baris pertama menunjukkan 100 permintaan yang tersedia karena belum ada segmen sebelumnya.

Waktu Tersedia Diambil Didaur ulang dari produk yang sudah kedaluwarsa Melanjutkan
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 Melanjutkan
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 sebanyak 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();

Partisi untuk pembatasan laju

Partisi pembatasan laju membagi lalu lintas menjadi wadah terpisah yang masing-masing mendapatkan penghitung batas lajunya sendiri. Pendekatan ini memberikan kontrol yang lebih terperinci daripada satu penghitung global. Kunci yang berbeda, seperti ID pengguna, alamat IP, atau kunci API, menentukan wadah partisi.

Manfaat pemartisian

  • Kewajaran: Satu pengguna tidak dapat menggunakan seluruh batas tarif untuk semua orang.
  • Granularitas: Batas yang berbeda untuk pengguna dan sumber daya yang berbeda.
  • Keamanan: Perlindungan yang lebih baik terhadap penyalahgunaan yang ditargetkan.
  • Layanan berjenjang: Dukungan untuk tingkat layanan dengan batas yang berbeda.

Pembatasan tarif yang dipartisi memberi Anda kontrol mendetail atas cara Anda mengelola lalu lintas API sambil memastikan alokasi sumber daya yang adil.

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

Menurut identitas pengguna

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

Berdasarkan kunci 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)
            }),
    };
});

Menurut jalur titik akhir

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

Membuat pembatas berantai

CreateChained API menerima beberapa PartitionedRateLimiter instans dan menggabungkannya menjadi satu PartitionedRateLimiter. Pembatas gabungan menjalankan semua pembatas input secara berurutan. Karena hasilnya adalah sebuah PartitionedRateLimiter yang ditetapkan ke GlobalLimiter, rantai tersebut diterapkan ke setiap endpoint. Untuk merangkai limiter untuk endpoint tertentu, gunakan kebijakan bernama, seperti yang ditunjukkan dalam Merangkai limiter dalam kebijakan bernama.

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

Untuk informasi selengkapnya, lihat kode sumber CreateChained.

Pembatas rantai dalam kebijakan bernama

CreateChained menautkan pembatas global yang berlaku untuk setiap titik akhir. Untuk menggabungkan beberapa jenis limiter dan menerapkannya pada endpoint tertentu, rangkai limiter tersebut di dalam kebijakan bernama dengan CreateChained. Metode overload ini mengembalikan satu RateLimiter yang menjalankan setiap limiter secara berurutan, yang merupakan tipe pengembalian yang diperlukan oleh pabrik partisi dari kebijakan bernama.

Kebijakan berikut menautkan "combined" pembatas wadah token dan pembatas konkurensi dengan RateLimiter.CreateChained, lalu menerapkan kebijakan ke satu titik akhir dengan 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");

Permintaan harus mendapatkan lease dari setiap limiter dalam rantai agar dapat diproses, dan limiter dijalankan sesuai urutan yang diteruskan ke RateLimiter.CreateChained. Jika pembatas laju menolak permintaan, permintaan tersebut akan ditolak, dan lease yang sudah diperoleh dari pembatas sebelumnya dalam rantai akan dilepas dalam urutan terbalik.

Saat menautkan pembatas dalam kebijakan bernama, ingatlah hal berikut:

  • Melepas lease akan mengembalikan permit untuk pembatas konkurensi. Pembatas berbasis waktu (bucket token, jendela tetap, dan jendela geser) tidak mengembalikan izin yang sudah diperoleh ketika pembatas berikutnya dalam rantai menolak permintaan, jadi, pertimbangkan hal ini saat menentukan urutan pembatas dalam rantai.
  • Mengonstruksi TokenBucketRateLimiter secara langsung dengan AutoReplenishment diatur ke true memberi setiap instans pembatas pengatur waktunya sendiri. Pembantu AddTokenBucketLimiter dan RateLimitPartition.GetTokenBucketLimiter sebagai gantinya mengatur AutoReplenishment ke false dan mengisi semua pembatas mereka dari satu timer bersama.
  • RateLimiter.CreateChained tidak melepaskan pembatas yang diteruskan kepadanya. Dalam contoh sebelumnya, partisi menyimpan pembatas berantai dan kerangka kerja mengelola masa pakainya. Jika Anda membuat pembatas berantai di luar partisi, buang pembatas dalam saat tidak lagi digunakan.
  • Lebih suka pendekatan global PartitionedRateLimiter.CreateChained ketika rantai harus berlaku untuk setiap titik akhir. Gunakan kebijakan bernama dengan RateLimiter.CreateChained hanya jika rantai harus dibatasi pada endpoint tertentu.

Memilih apa yang terjadi ketika permintaan dibatasi oleh batas kecepatan

Untuk kasus sederhana, Anda hanya dapat mengatur kode status:

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

    // Rate limiter configuration...
});

Pendekatan yang paling umum adalah mendaftarkan callback OnRejected saat mengonfigurasi pembatasan laju:

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

Opsi lain adalah mengantrekan permintaan:

Meminta antrean

Saat Anda mengaktifkan antrean, jika permintaan melebihi batas tarif, sistem menempatkannya dalam antrean. Permintaan menunggu di antrean hingga izin tersedia atau batas waktu habis. Sistem memproses permintaan sesuai dengan urutan antrean yang dapat dikonfigurasi.

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

EnableRateLimiting dan atribut DisableRateLimiting

Terapkan [EnableRateLimiting] atribut dan [DisableRateLimiting] ke pengontrol, metode tindakan, atau Razor Halaman. Untuk Razor Pages, terapkan atribut pada Razor Page, bukan pada handler halaman. Misalnya, Anda tidak dapat menerapkan [EnableRateLimiting] ke OnGet, OnPost, atau handler halaman lainnya.

Atribut [DisableRateLimiting]menonaktifkan pembatasan laju akses untuk kontroler, metode aksi, atau Razor halaman, apa pun pembatas laju akses bernama atau pembatas global yang diterapkan. Misalnya, pertimbangkan kode berikut yang memanggil RequireRateLimiting untuk menerapkan fixedPolicy pembatasan laju ke semua endpoint 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 menimpa [EnableRateLimiting("fixed")] yang diterapkan ke Home2Controller dan dipanggil dalam app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) pada 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 metode aksi Privacy karena Program.cs dipanggil app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Pertimbangkan kode berikut yang tidak memanggil RequireRateLimiting di 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 tingkat kebijakan "fixed" diterapkan ke semua metode tindakan yang tidak memiliki atribut EnableRateLimiting dan DisableRateLimiting.
  • Pembatas tarif kebijakan "sliding" diterapkan pada tindakan Privacy.
  • Pembatasan laju dinonaktifkan pada NoLimit metode aksi.

Metrik pembatasan laju

Middleware pembatasan laju menyediakan metrik bawaan dan kemampuan pemantauan untuk membantu Anda memahami bagaimana batas laju memengaruhi performa aplikasi dan pengalaman pengguna. Untuk daftar metrik, lihat Microsoft.AspNetCore.RateLimiting.

Menguji titik akhir dengan pembatasan kecepatan

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

Jika Anda membuat partisi dengan menggunakan input pengguna, aplikasi Anda menjadi rentan terhadap serangan Penolakan Layanan (DoS). Misalnya, jika Anda membuat partisi berdasarkan alamat IP klien, aplikasi Anda menjadi rentan terhadap serangan Denial of Service yang menggunakan spoofing alamat sumber IP. Untuk informasi selengkapnya, lihat Pemfilteran Masuk Jaringan BCP 38 RFC 2827: Mengalahkan Serangan Penolakan Layanan yang menggunakan pemalsuan alamat sumber IP.

Sumber Daya Tambahan: