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:
- AddRateLimiter Panggilan untuk menambahkan layanan pembatasan tarif ke koleksi layanan.
- Panggilan untuk membuat pembatas
AddFixedWindowLimiter
jendela tetap dengan nama"fixed"
kebijakan dan set: - PermitLimit ke 4 dan waktu Window ke 12. Maksimum 4 permintaan per setiap jendela 12 detik diizinkan.
- QueueProcessingOrder ke OldestFirst.
- QueueLimit ke 2.
- Memanggil UseRateLimiter untuk mengaktifkan pembatasan tarif.
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 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
, , OnPost
atau 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 memilikiEnableRateLimiting
atribut danDisableRateLimiting
. - Pembatas
"sliding"
tarif kebijakan diterapkan padaPrivacy
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
, , OnPost
atau handler halaman lainnya.
Atribut DisableRateLimiting
menonaktifkan pembatasan tarif pada Razor Halaman. EnableRateLimiting
hanya 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 denganTokenBucketRateLimiter
,FixedWindowLimiter
, danSlidingWindowLimiter
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
mengimplementasikanIRateLimiterPolicy<TPartitionKey>
antarmuka. KelasSampleRateLimiterPolicy
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.
- Yang
// 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:
ConcurrencyLimiter
Menambahkan dengan nama"get"
kebijakan yang digunakan pada Razor Halaman.TokenBucketRateLimiter
Menambahkan dengan partisi untuk setiap pengguna yang berwenang dan partisi untuk semua pengguna anonim.- Mengatur RateLimiterOptions.RejectionStatusCode ke 429 Terlalu Banyak Permintaan.
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:
- Middleware pembatasan laju oleh Maarten Balliauw memberikan pengenalan dan gambaran umum yang sangat baik untuk pembatasan tarif.
- Batas laju handler HTTP di .NET
ASP.NET Core