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 waktuwindow
hingga 12. Maksimal 4 permintaan per setiap jendela 12 detik diperbolehkan.queueProcessingOrder
keQueueProcessingOrder.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 true
penghitung waktu internal mengisi ulang token setiap replenishmentPeriod
; ketika diatur ke false
aplikasi 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
, RetryAfter
dan GlobalLimiter
Contoh berikut:
-
Membuat callback RateLimiterOptions.OnRejected yang dipanggil saat permintaan melebihi batas yang ditentukan.
retryAfter
dapat digunakan denganTokenBucketRateLimiter
,FixedWindowLimiter
danSlidingWindowLimiter
karena algoritme ini dapat memperkirakan kapan lebih banyak izin akan ditambahkan. ItuConcurrencyLimiter
tidak memiliki cara untuk menghitung kapan izin akan tersedia. -
Menambahkan pembatas berikut:
- SEBUAH
SampleRateLimiterPolicy
yang mengimplementasikanIRateLimiterPolicy<TPartitionKey>
antarmuka. ItuSampleRateLimiterPolicy
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.
- SEBUAH
// 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
, TokenBucketRateLimiter
dan 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.