Nguon: Microsoft Learn · .NET 8.0

Middleware giới hạn tốc độ (Rate Limiting) trong ASP.NET Core

Nguồn: Rate limiting middleware in ASP.NET Core

Tác giả: Arvin Kahbazi, Maarten Balliauw, và Rick Anderson

Middleware Microsoft.AspNetCore.RateLimiting cung cấp tính năng rate limiting (giới hạn tốc độ). Ứng dụng cấu hình các rate limiting policy (chính sách giới hạn tốc độ) và sau đó đính kèm các policy vào các endpoint (điểm cuối). Các ứng dụng sử dụng rate limiting nên được kiểm tra tải kỹ lưỡng và xem xét trước khi triển khai.

Tại sao sử dụng rate limiting

Rate limiting có thể được dùng để quản lý luồng các request đến ứng dụng. Các lý do chính để triển khai rate limiting:

Ngăn chặn tấn công DDoS

Mặc dù rate limiting có thể giúp giảm thiểu rủi ro tấn công Denial of Service (DoS), nhưng nó không phải là giải pháp toàn diện cho các cuộc tấn công Distributed Denial of Service (DDoS - Từ chối dịch vụ phân tán). Các cuộc tấn công DDoS liên quan đến nhiều hệ thống áp đảo ứng dụng bằng một lượng lớn request, khiến khó xử lý chỉ bằng rate limiting.

Để bảo vệ DDoS mạnh mẽ, hãy xem xét sử dụng dịch vụ bảo vệ DDoS thương mại. Khi sử dụng dịch vụ cloud hosting, bảo vệ DDoS thường có sẵn như một phần của giải pháp hosting, chẳng hạn như Azure Web Application Firewall, AWS Shield hoặc Google Cloud Armor.

Sử dụng Rate Limiting Middleware

Các bước sau đây cho thấy cách sử dụng rate limiting middleware trong ứng dụng ASP.NET Core:

  1. Cấu hình rate limiting service (dịch vụ giới hạn tốc độ).

Trong file Program.cs, cấu hình các rate limiting service bằng cách thêm các rate limiting policy phù hợp. Policy có thể được định nghĩa là global (toàn cục) hoặc named (có tên). Ví dụ sau cho phép 10 request mỗi phút theo user (người dùng) (identity - danh tính) hoặc toàn cục:

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

Named policy cần được áp dụng rõ ràng cho các trang hoặc endpoint. Ví dụ sau thêm một fixed window limiter policy có tên "fixed" mà chúng ta sẽ thêm vào một endpoint sau:

csharp
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();
  1. Kích hoạt rate limiting middleware

Trong file Program.cs, kích hoạt rate limiting middleware bằng cách gọi UseRateLimiter:

csharp
app.UseRouting();

app.UseRateLimiter();

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

app.Run();

Áp dụng rate limiting policy cho endpoint hoặc trang

Áp dụng rate limiting cho WebAPI Endpoint

Áp dụng named policy cho endpoint hoặc group (nhóm), ví dụ:

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

Áp dụng rate limiting cho MVC Controller

Áp dụng các rate limiting policy đã cấu hình cho các endpoint cụ thể hoặc toàn cục. Ví dụ, để áp dụng policy "fixed" cho tất cả các controller endpoint:

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

Áp dụng rate limiting cho server-side Blazor app

Để đặt rate limiting cho tất cả các Razor component có thể route trong ứng dụng, chỉ định RequireRateLimiting với tên rate limiting policy trên lệnh gọi MapRazorComponents trong file Program. Trong ví dụ sau, rate limiting policy có tên "policy" được áp dụng:

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

Để đặt policy cho một Razor component có thể route hoặc một thư mục component thông qua file _Imports.razor, thuộc tính [EnableRateLimiting] được áp dụng với tên policy. Trong ví dụ sau, rate limiting policy có tên "override" được áp dụng. Policy này thay thế bất kỳ policy nào hiện đang áp dụng cho endpoint. Global limiter vẫn chạy trên endpoint với thuộc tính này được áp dụng.

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

<h1>Counter</h1>

Thuộc tính [DisableRateLimiting] được sử dụng để vô hiệu hóa rate limiting cho một Razor component có thể route hoặc một thư mục component thông qua file _Imports.razor.

Các thuật toán Rate Limiter

Class RateLimiterOptionsExtensions cung cấp các extension method sau cho rate limiting:

Các limiter fixed, sliding và token đều giới hạn số lượng request tối đa trong một khoảng thời gian. Concurrency limiter chỉ giới hạn số lượng request đồng thời và không giới hạn số lượng request trong một khoảng thời gian. Chi phí của một endpoint nên được xem xét khi chọn limiter. Chi phí của một endpoint bao gồm các tài nguyên được sử dụng, ví dụ thời gian, truy cập dữ liệu, CPU và I/O.

Fixed window limiter (Bộ giới hạn cửa sổ cố định)

Phương thức AddFixedWindowLimiter sử dụng một cửa sổ thời gian cố định để giới hạn request. Khi cửa sổ thời gian hết hạn, một cửa sổ thời gian mới bắt đầu và giới hạn request được đặt lại.

Xem xét code sau:

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

Code trên:

Ứng dụng nên sử dụng Configuration để đặt các tùy chọn limiter.

UseRateLimiter phải được gọi sau UseRouting khi sử dụng các API rate limiting endpoint-specific. Ví dụ, nếu thuộc tính [EnableRateLimiting] được sử dụng, UseRateLimiter phải được gọi sau UseRouting. Khi chỉ gọi global limiter, UseRateLimiter có thể được gọi trước UseRouting.

Sliding window limiter (Bộ giới hạn cửa sổ trượt)

Thuật toán sliding window:

Bảng sau đây hiển thị sliding window limiter với cửa sổ 30 giây, ba segment mỗi cửa sổ và giới hạn 100 request:

Thời gianSẵn cóĐược lấyTái chế từ expiredMang sang
010020080
108030050
205040010
301030200
400103020
5020104050
6050353045

Code sau sử dụng sliding window rate limiter:

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

Token bucket limiter (Bộ giới hạn xô token)

Token bucket limiter tương tự như sliding window limiter, nhưng thay vì thêm lại các request được lấy từ expired segment, một số lượng token (thẻ) cố định được thêm vào mỗi replenishment period (kỳ bổ sung). Các token được thêm vào mỗi segment không thể tăng token sẵn có lên số lượng cao hơn giới hạn token bucket. Bảng sau đây hiển thị token bucket limiter với giới hạn 100 token và kỳ bổ sung 10 giây:

Thời gianSẵn cóĐược lấyĐược thêmMang sang
010020080
1080102090
2090515100
30100302090
4090616100
50100402080
6080502050

Code sau sử dụng token bucket limiter:

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

Khi AutoReplenishment được đặt thành true, một internal timer (bộ đếm thời gian nội bộ) bổ sung token sau mỗi ReplenishmentPeriod; khi đặt thành false, ứng dụng phải gọi TryReplenish trên limiter.

Concurrency limiter (Bộ giới hạn đồng thời)

Concurrency limiter giới hạn số lượng request đồng thời. Mỗi request giảm giới hạn concurrency (đồng thời) đi một. Khi một request hoàn thành, giới hạn tăng lên một. Không giống như các request limiter khác giới hạn tổng số request trong một khoảng thời gian cụ thể, concurrency limiter chỉ giới hạn số lượng request đồng thời và không giới hạn số lượng request trong một khoảng thời gian.

Code sau sử dụng concurrency limiter:

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

Rate Limiting Partition (Phân vùng giới hạn tốc độ)

Rate limiting partition (phân vùng giới hạn tốc độ) chia lưu lượng thành các "bucket" (xô) riêng biệt, mỗi bucket có bộ đếm rate limit riêng. Điều này cho phép kiểm soát chi tiết hơn so với một bộ đếm toàn cục. Các "bucket" phân vùng được xác định bởi các key khác nhau (như user ID, địa chỉ IP, hoặc API key).

Theo địa chỉ IP

csharp
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        }));

Theo danh tính người dùng

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

Theo API Key

csharp
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string apiKey = httpContext.Request.Headers["X-API-Key"].ToString() ?? "no-key";

    // Giới hạn khác nhau dựa trên tier của key
    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)
            }),
    };
});

Tạo chained limiter (bộ giới hạn dây chuyền)

API CreateChained cho phép truyền vào nhiều PartitionedRateLimiter được kết hợp thành một PartitionedRateLimiter. Combined limiter (bộ giới hạn kết hợp) chạy tất cả các input limiter theo trình tự.

Code sau sử dụng CreateChained:

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

Lựa chọn hành động khi request bị giới hạn tốc độ

Đối với các trường hợp đơn giản, bạn có thể chỉ đặt status code:

csharp
builder.Services.AddRateLimiter(options =>
{
    // Đặt status code tùy chỉnh cho các request bị từ chối
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Cấu hình rate limiter...
});

Cách phổ biến nhất là đăng ký callback OnRejected khi cấu hình rate limiting:

csharp
builder.Services.AddRateLimiter(options =>
{
    // Cấu hình rate limiter...

    options.OnRejected = async (context, cancellationToken) =>
    {
        // Logic xử lý từ chối tùy chỉnh
        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);

        // Ghi log tùy chọn
        logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
            context.HttpContext.Connection.RemoteIpAddress);
    };
});

Request Queuing (Xếp hàng Request)

Với queuing được kích hoạt, khi một request vượt quá giới hạn tốc độ, nó được đặt vào một queue (hàng đợi) nơi request chờ cho đến khi có permit (giấy phép) khả dụng hoặc cho đến khi timeout (hết thời gian) xảy ra.

csharp
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", options =>
    {
        options.PermitLimit = 10;           // Cho phép 10 request
        options.Window = TimeSpan.FromSeconds(10);  // Mỗi cửa sổ 10 giây
        options.QueueLimit = 5;             // Xếp hàng tối đa 5 request bổ sung
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Xử lý request cũ nhất trước
        options.AutoReplenishment = true; // Mặc định: tự động bổ sung permit
    });
});

Các thuộc tính EnableRateLimitingDisableRateLimiting

Các thuộc tính [EnableRateLimiting][DisableRateLimiting] có thể được áp dụng cho Controller, action method, hoặc Razor Page. Đối với Razor Pages, thuộc tính phải được áp dụng cho Razor Page chứ không phải các page handler. Ví dụ, [EnableRateLimiting] không thể được áp dụng cho OnGet, OnPost, hoặc bất kỳ page handler nào khác.

Thuộc tính [DisableRateLimiting] vô hiệu hóa rate limiting cho Controller, action method, hoặc Razor Page bất kể named rate limiter hoặc global limiter nào được áp dụng. Ví dụ, xem xét code sau gọi RequireRateLimiting để áp dụng rate limiting fixedPolicy cho tất cả các controller endpoint:

csharp
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);

Trong code sau, [DisableRateLimiting] vô hiệu hóa rate limiting và ghi đè [EnableRateLimiting("fixed")] được áp dụng cho Home2Controller:

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

Trong controller trên:

Metrics của Rate Limiting

Rate limiting middleware cung cấp các khả năng metrics và monitoring (giám sát) tích hợp sẵn để giúp hiểu rate limit ảnh hưởng đến hiệu suất ứng dụng và trải nghiệm người dùng như thế nào.

Kiểm tra endpoint với rate limiting

Trước khi triển khai ứng dụng sử dụng rate limiting vào môi trường production (sản xuất), hãy stress test (kiểm tra tải) ứng dụng để xác thực các rate limiter và tùy chọn được sử dụng. Ví dụ, tạo JMeter script với một công cụ như BlazeMeter và tải script vào Azure Load Testing.

Tạo phân vùng với user input (đầu vào người dùng) làm ứng dụng dễ bị Denial of Service (DoS) Attacks. Ví dụ, tạo phân vùng theo địa chỉ IP client làm ứng dụng dễ bị tấn công Denial of Service sử dụng IP Source Address Spoofing (giả mạo địa chỉ nguồn IP).