Asp-Net-Core開發筆記:使用RateLimit中介軟體實現介面限流

程式設計實驗室發表於2023-03-17

前言

最近一直在忙(2月份沉迷steam,3月開始工作各種忙),好久沒更新部落格了,不過也積累了一些,忙裡偷閒記錄一下。

這個需求是這樣的,我之前做了個工單系統,現在要對登入、註冊、發起工單這些功能做限流,不能讓使用者請求太頻繁。

從 .Net7 開始,已經有內建的限流功能了,但目前我們的專案還在使用 .Net6 LTS 版本,下一個 LTS 沒釋出之前,暫時不考慮使用 .Net7 這種非 LTS 版本。

然後我找到了這個 AspNetCoreRateLimit 元件,在 Github 上有接近三千個星星,看了一下檔案使用也簡單靈活,於是決定嘗試一下~

AspNetCoreRateLimit 元件

專案主頁: https://github.com/stefanprodan/AspNetCoreRateLimit

這是官方的介紹:

AspNetCoreRateLimit is an ASP.NET Core rate limiting solution designed to control the rate of requests that clients can make to a Web API or MVC app based on IP address or client ID.

The AspNetCoreRateLimit NuGet package contains an IpRateLimitMiddleware and a ClientRateLimitMiddleware, with each middleware you can set multiple limits for different scenarios like allowing an IP or Client to make a maximum number of calls in a time interval like per second, 15 minutes, etc. You can define these limits to address all requests made to an API or you can scope the limits to each API URL or HTTP verb and path.

用最近很厲害的 ChatGPT 翻譯一下:

AspNetCoreRateLimit是一個ASP.NET Core速率限制解決方案,旨在基於IP地址或客戶端ID控制客戶端對Web API或MVC應用程式發出請求的速率。

AspNetCoreRateLimit NuGet包 包含一個IpRateLimitMiddleware和一個ClientRateLimitMiddleware,每個中介軟體都可以為不同的場景設定多個限制,比如允許IP或客戶端在時間間隔內進行最大數量的呼叫,比如每秒、15分鐘等。您可以定義這些限制以處理對API發出的所有請求,也可以將限制範圍限定為每個API URL或HTTP動詞和路徑。

這個元件使用起來挺靈活的,直接在 AspNetCore配置 裡定義規則,意味著可以不重新編譯程式就修改限流規則,官方給的例子是直接在 appsettings.json 裡配置,但使用其他配置源理論上也沒問題(配置中心用起來)。

簡單介紹下這個元件的思路

首先它有兩種模式:

  • 根據IP地址限流
  • 根據 ClientID 限流

IP地址很容易理解,ClientID 我一開始以為是使用者ID,不過看了說明,是一個放在請求頭裡的引數,比如 X-ClientId,這個要自己實現,可以直接用使用者ID。

為了方便使用,我這個專案裡面直接用IP地址模式。

RateLimit 元件可以配置全域性的限流,也可以配置對某個IP地址(段)進行限流。

配置服務

為了從 appsettings.json 讀取資料,先在 Program.cs 註冊配置服務

builder.Services.AddOptions();

然後寫個擴充套件方法來註冊 RateLimit 的相關服務

引入名稱空間

using AspNetCoreRateLimit;
using AspNetCoreRateLimit.Redis;
using StackExchange.Redis;

寫個靜態類

public static class ConfigureRateLimit {
    public static void AddRateLimit(this IServiceCollection services, IConfiguration conf) {
        //load general configuration from appsettings.json
        services.Configure<IpRateLimitOptions>(conf.GetSection("IpRateLimiting"));

        var redisOptions = ConfigurationOptions.Parse(conf.GetConnectionString("Redis"));
        services.AddSingleton<IConnectionMultiplexer>(provider => ConnectionMultiplexer.Connect(redisOptions));
        services.AddRedisRateLimiting();

        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
    }

    public static IApplicationBuilder UseRateLimit(this IApplicationBuilder app) {
        app.UseIpRateLimiting();

        return app;
    }
}

來解析一下配置的程式碼。

我暫時不需要對不同的IP地址段應用不同的限流規則

所以直接用 IpRateLimitOptions

services.Configure<IpRateLimitOptions>(conf.GetSection("IpRateLimiting"));

要做根據IP限流,就得記錄每個IP訪問了多少次,RateLimit 元件支援多種儲存方式,最簡單的可以直接存記憶體裡,不過為了穩定我還是選擇 Redis。

這幾行程式碼就是配置 Redis 的。

var redisOptions = ConfigurationOptions.Parse(conf.GetConnectionString("Redis"));
services.AddSingleton<IConnectionMultiplexer>(provider => ConnectionMultiplexer.Connect(redisOptions));
services.AddRedisRateLimiting();

最後注入一下 IRateLimitConfiguration,我猜應該是中介軟體要用到的。至少我目前在 Controller 程式碼裡不需要用到任何跟 RateLimit 有關的程式碼。

寫完了擴充套件方法,回到 Program.cs

註冊服務

builder.Services.AddRateLimit(builder.Configuration);

新增中介軟體

var app = builder.Build();

app.UseExceptionless();
app.UseStaticFiles(new StaticFileOptions {
    ServeUnknownFileTypes = true
});
app.UseRateLimit();

// ...

app.Run();

我這裡把 UseRateLimit 放在 UseStaticFiles 後面,不然頁面裡的靜態檔案都被算進去訪問次數,很快就被限流了。

配置

appsettings.json 裡寫具體的限流規則。

官網提供的配置規則不能照抄,要理解一下他的檔案

  • EnableEndpointRateLimiting - 這個選項要設定為 true ,不然設定的限流是全域性的,不能根據某個路徑單獨設定限流
  • StackBlockedRequests - 按照預設的設定為 false 就行,設定成 true 的話,一個介面被限流之後再重複請求還會計算到訪問次數裡面,這樣有可能導致限流到天荒地老。

其他的配置顧名思義,懂的都懂。

GeneralRules 是對具體路徑的限流規則

如果全侷限流,把 EnableEndpointRateLimiting 設定為 false 的話,那就這樣設定,1分鐘只能訪問5次

{
    "Endpoint": "*",
    "Period": "1m",
    "Limit": 5
}

Endpoint 可以設定 HTTP方法:路徑 的形式,比如 post:/account/login 具體看檔案吧(參考檔案第三條)

附上我的配置檔案,對新增工單、登入、註冊介面進行限流。

{
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "IpWhitelist": [],
    "EndpointWhitelist": [
      "get:/api/license",
      "*:/api/status"
    ],
    "ClientWhitelist": [
      "dev-id-1",
      "dev-id-2"
    ],
    "GeneralRules": [
      {
        "Endpoint": "*:/ticket/add",
        "Period": "1m",
        "Limit": 5
      },
      {
        "Endpoint": "post:/account/login",
        "Period": "1m",
        "Limit": 5
      },
      {
        "Endpoint": "post:/account/SignUp",
        "Period": "1m",
        "Limit": 5
      }
    ],
    "QuotaExceededResponse": {
      "Content": "{{ \"message\": \"先別急,你訪問得太快了!\", \"details\": \"已經觸發限流。限流規則: 每 {1} 只能訪問 {0} 次。請 {2} 秒後再重試。\" }}",
      "ContentType": "application/json",
      "StatusCode": 429
    }
  }
}

同時自定義了被限流時的提示。

效果如下

{
  "message": "先別急,你訪問得太快了!",
  "details": "已經觸發限流。限流規則: 每 1m 只能訪問 5 次。請 16 秒後再重試。"
}

參考資料

相關文章