Itu Microsoft.AspNetCore.RateLimiting middleware menyediakan middleware pembatas tingkat. Aplikasi mengonfigurasi kebijakan pembatasan tarif, lalu melampirkan kebijakan ke titik akhir. Aplikasi yang menggunakan pembatasan kecepatan harus diuji dan ditinjau dengan cermat sebelum diterapkan. Lihat Menguji titik akhir dengan pembatasan kecepatan dalam artikel ini untuk informasi selengkapnya.

Algoritme pembatas nilai

Itu RateLimiterOptionsExtensions class menyediakan metode ekstensi berikut untuk pembatasan tarif:

Pembatas jendela tetap

Itu AddFixedWindowLimiter metode menggunakan jendela waktu tetap untuk membatasi permintaan. Ketika jendela waktu berakhir, jendela waktu baru dimulai dan batas permintaan diatur ulang.

Perhatikan kode berikut:

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

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.UseRateLimiter(new RateLimiterOptions()
    .AddFixedWindowLimiter(policyName: "fixed",
          new FixedWindowRateLimiterOptions(permitLimit: 4,
          window: TimeSpan.FromSeconds(12),
          queueProcessingOrder: QueueProcessingOrder.OldestFirst,
          queueLimit: 2)));

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

app.Run();

Kode sebelumnya:

  • Memanggil UseRateLimiter untuk mengaktifkan pembatasan tarif.
  • Membuat pembatas jendela tetap dengan nama kebijakan "fixed" dan set:
  • permitLimit ke 4 dan waktu window hingga 12. Maksimal 4 permintaan per setiap jendela 12 detik diperbolehkan.
  • queueProcessingOrder ke QueueProcessingOrder.OldestFirst.
  • queueLimit ke 2.

Aplikasi harus menggunakan Konfigurasi untuk menyetel 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 app = builder.Build();

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

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

app.UseRateLimiter(new RateLimiterOptions()
    .AddFixedWindowLimiter(policyName: fixedPolicy,
          new FixedWindowRateLimiterOptions(permitLimit: myOptions.permitLimit,
          window: TimeSpan.FromSeconds(myOptions.window),
          queueProcessingOrder: QueueProcessingOrder.OldestFirst,
          queueLimit: myOptions.queueLimit)));

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

app.Run();

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 (waktu jendela)/(segmen per jendela).

  • Batasi permintaan untuk sebuah jendela hingga permitLimit permintaan.

  • Setiap jendela waktu dibagi menjadi n segmen per jendela.

  • Permintaan yang diambil dari segmen waktu kedaluwarsa satu jendela ke belakang (n segmen sebelum segmen saat ini), ditambahkan ke segmen saat ini. Kami merujuk ke segmen waktu yang paling kedaluwarsa satu jendela kembali sebagai segmen kedaluwarsa. Perhatikan tabel berikut yang menunjukkan pembatas jendela geser dengan jendela 30 detik, 3 segmen per jendela dan batas 100 permintaan:

  • Baris atas dan kolom pertama menunjukkan segmen waktu.

  • Baris kedua menunjukkan sisa permintaan yang tersedia. Permintaan yang tersisa tersedia-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 berikut menunjukkan data pada grafik sebelumnya dalam format yang berbeda. Itu Tersisa kolom menunjukkan permintaan yang tersedia dari segmen sebelumnya (The Menopang dari baris sebelumnya). Baris pertama menunjukkan 100 tersedia karena tidak ada segmen sebelumnya:

Waktu Tersedia Diambil Didaur ulang dari kadaluarsa Menopang
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 kecepatan jendela geser:

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

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

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

app.UseRateLimiter(new RateLimiterOptions()
    .AddSlidingWindowLimiter(policyName: slidingPolicy,
          new SlidingWindowRateLimiterOptions(permitLimit: myOptions.permitLimit,
          window: TimeSpan.FromSeconds(myOptions.window),
          segmentsPerWindow: myOptions.segmentsPerWindow,
          queueProcessingOrder: QueueProcessingOrder.OldestFirst,
          queueLimit: myOptions.queueLimit)));

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

app.Run();

Pembatas ember token

Pembatas ember token mirip dengan pembatas jendela geser, tetapi alih-alih menambahkan kembali permintaan yang diambil dari segmen yang kedaluwarsa, sejumlah 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 ember token. Tabel berikut menunjukkan pembatas ember token dengan batas 100 token dan periode pengisian 10 detik:

Waktu Tersedia Diambil Ditambahkan Menopang
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 ember token:

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

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

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

app.UseRateLimiter(new RateLimiterOptions()
    .AddTokenBucketLimiter(policyName: tokenPolicy,
          new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit,
                queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                queueLimit: myOptions.queueLimit,
                replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
                tokensPerPeriod: myOptions.tokensPerPeriod,
                autoReplenishment: myOptions.autoReplenishment)));

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

app.Run();

Kapan autoReplenishment diatur ke truepenghitung waktu internal mengisi ulang token setiap replenishmentPeriod; ketika diatur ke falseaplikasi harus memanggil TryReplenish pada pembatas.

Pembatas konkurensi

Pembatas konkurensi membatasi jumlah permintaan serentak. Setiap permintaan mengurangi batas konkurensi satu per satu. Saat permintaan selesai, batasnya bertambah satu. Tidak seperti pembatas permintaan lainnya yang membatasi jumlah total permintaan untuk jangka waktu 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 app = builder.Build();

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

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

app.UseRateLimiter(new RateLimiterOptions()
    .AddConcurrencyLimiter(policyName: concurrencyPolicy,
          new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit,
          queueProcessingOrder: QueueProcessingOrder.OldestFirst,
          queueLimit: myOptions.queueLimit)));

app.MapGet("/", async () =>

    await Task.Delay(500);
    return Results.Ok($"Concurrency Limiter GetTicks()");
                              
).RequireRateLimiting(concurrencyPolicy);

app.Run();

Perbandingan algoritma pembatas

Pembatas tetap, geser, dan token semuanya membatasi jumlah maksimum permintaan dalam jangka waktu tertentu. Pembatas konkurensi hanya membatasi jumlah permintaan serentak 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 nilai

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

Pembatas dengan OnRejected, RetryAfterdan GlobalLimiter

Contoh berikut:

  • Membuat callback RateLimiterOptions.OnRejected yang dipanggil saat permintaan melebihi batas yang ditentukan. retryAfter dapat digunakan dengan TokenBucketRateLimiter, FixedWindowLimiterdan SlidingWindowLimiter karena algoritme ini dapat memperkirakan kapan lebih banyak izin akan ditambahkan. Itu ConcurrencyLimiter tidak memiliki cara untuk menghitung kapan izin akan tersedia.

  • Menambahkan pembatas berikut:

    • SEBUAH SampleRateLimiterPolicy yang mengimplementasikan IRateLimiterPolicy<TPartitionKey> antarmuka. Itu SampleRateLimiterPolicy kelas ditampilkan nanti dalam artikel ini.
    • SEBUAH SlidingWindowLimiter:
      • Dengan partisi untuk setiap pengguna yang diautentikasi.
      • Satu partisi bersama untuk semua pengguna anonim.
    • Sebuah GlobalLimiter yang diterapkan untuk semua permintaan. Pembatas global akan dieksekusi terlebih dahulu, diikuti oleh pembatas khusus titik akhir, jika ada. Itu GlobalLimiter membuat partisi untuk setiap alamat IP.
// Preceding code removed for brevity.

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

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

var options = new RateLimiterOptions()

    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(context.HttpContext)");

        return new ValueTask();
    

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

        return RateLimitPartition.CreateSlidingWindowLimiter<string>(username,
              key => new SlidingWindowRateLimiterOptions(
              permitLimit: myOptions.permitLimit,
              queueProcessingOrder: QueueProcessingOrder.OldestFirst,
              queueLimit: myOptions.queueLimit,
              window: TimeSpan.FromSeconds(myOptions.window),
              segmentsPerWindow: myOptions.segmentsPerWindow
            ));

    );

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>

    IPAddress? remoteIPaddress = context?.Connection?.RemoteIpAddress;

    if (!IPAddress.IsLoopback(remoteIPaddress!))
    
        return RateLimitPartition.CreateTokenBucketLimiter<IPAddress>
           (remoteIPaddress!, key =>
                 new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
                     queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                     queueLimit: myOptions.queueLimit,
                     replenishmentPeriod: 
                                 TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
                     tokensPerPeriod: myOptions.tokensPerPeriod,
                     autoReplenishment: myOptions.autoReplenishment));
    
    else
    
        return RateLimitPartition.CreateNoLimiter<IPAddress>(IPAddress.Loopback);
    
);

app.UseRateLimiter(options);

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

Lihat repositori sampel untuk lengkapnya Program.cs mengajukan.

Itu SampleRateLimiterPolicy kelas

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  get => _onRejected; 

    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    
        return RateLimitPartition.CreateSlidingWindowLimiter<string>(string.Empty, 
            key => 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 penolakan default adalah 503 Layanan Tidak Tersedia.

Pembatas dengan otorisasi

Contoh berikut menggunakan JSON Web Tokens (JWT) dan membuat partisi dengan token akses JWT. Dalam aplikasi produksi, JWT biasanya 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.AspNetCore.RateLimiting;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthorization();

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

var options = new RateLimiterOptions()

    RejectionStatusCode = StatusCodes.Status429TooManyRequests

     .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.CreateTokenBucketLimiter(accessToken, key =>
                 new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
                     queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                     queueLimit: myOptions.queueLimit,
                     replenishmentPeriod:
                                 TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
                     tokensPerPeriod: myOptions.tokensPerPeriod,
                     autoReplenishment: myOptions.autoReplenishment));
         
         else
         
             return RateLimitPartition.CreateTokenBucketLimiter("Anon", key =>
                 new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit,
                     queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                     queueLimit: myOptions.queueLimit,
                     replenishmentPeriod:
                                  TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
                     tokensPerPeriod: myOptions.tokensPerPeriod,
                     autoReplenishment: true));
         
     );

app.UseRateLimiter(options);

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, TokenBucketRateLimiterdan otorisasi

Contoh berikut:

var getPolicyName = "get";
var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

app.UseRateLimiter(new RateLimiterOptions()
    .AddConcurrencyLimiter(policyName: getPolicyName,
          new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit,
          queueProcessingOrder: QueueProcessingOrder.OldestFirst,
          queueLimit: myOptions.queueLimit))
    .AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
    
        string userName = httpContext?.User?.Identity?.Name ?? string.Empty;

        if (!StringValues.IsNullOrEmpty(userName))
        
            return RateLimitPartition.CreateTokenBucketLimiter(userName, key =>
                new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
                    queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                    queueLimit: myOptions.queueLimit,
                    replenishmentPeriod:
                                TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
                    tokensPerPeriod: myOptions.tokensPerPeriod,
                    autoReplenishment: myOptions.autoReplenishment));
        
        else
        
            return RateLimitPartition.CreateTokenBucketLimiter("Anon", key =>
                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 lengkapnya Program.cs mengajukan.

Menguji titik akhir dengan pembatasan kecepatan

Sebelum men-deploy aplikasi menggunakan pembatasan kecepatan untuk produksi, uji stres aplikasi untuk memvalidasi pembatas kecepatan 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 Denial of Service Attacks yang menggunakan IP Source Address Spoofing. Untuk informasi lebih lanjut, lihat BCP 38 RFC 2827 Network Ingress Filtering: Mengalahkan Serangan Denial of Service yang menggunakan IP Source Address Spoofing.

By AKDSEO