Contents

AspNetCoreRateLimit 使用小記

之前看很多樂觀鎖都是做防止修改同一筆資料,但最近同事之前用 RateLimit 預防前端遇到連點新增兩筆資料,這次專案我也類似用這個方法,前端程式我非能控制,這邊後端用這個方法做個補強。

Warning
.Net 7 有自己做 RateLimit,不一定要使用這個工具。這邊考慮用原生。
Built-in Rate Limiting support - part of .NET 7 · Issue #382 · stefanprodan/AspNetCoreRateLimit ,這邊我工作是 .Net 5專案,所以使用stefanprodan/AspNetCoreRateLimit: ASP.NET Core rate limiting middleware

心智圖

mindmap AspNetCoreRateLimit 官方內建方法 IpRateLimit ClientRateLimit Token based 驗證方法

IpRateLimit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	// needed to load configuration from appsettings.json
	// 一般專案都會有,可以省略
    // services.AddOptions();

	// needed to store rate limit counters and ip rules
	// 需要載入 MemoryCache,實際看你用哪個 Cache
    services.AddMemoryCache();

	//load general configuration from appsettings.json
	// 載入 IpRateLimit
    services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));

	//load ip rules from appsettings.json
    // 載入 Ip RateLimit 設定
	services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));

	// inject counter and rules stores
    // 載入 Memory Rate Limit
	services.AddInMemoryRateLimiting();
        //services.AddDistributedRateLimiting<AsyncKeyLockProcessingStrategy>();
        //services.AddDistributedRateLimiting<RedisProcessingStrategy>();
        //services.AddRedisRateLimiting();


        // configuration (resolvers, counter key builders)
        // 使用 IpRateLimit
        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
  1. UseIpRateLimiting
1
2
            app.UseAuthorization();
            app.UseIpRateLimiting();
Warning
雖然官方沒有講 UseIpRateLimiting 設定在哪裡,但需要加到UseAuthorization後面,我原本設定最上面,我API 超過次數 Response Http Status 回傳 0,後然突然想到我之前寫的這篇(.Net Core 設定 Middleware 引發前端錯誤問題 - 程式狂想筆記)。
  1. 設定檔案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": false,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ],
    "EndpointWhitelist": [ "get:/api/license", "*:/api/status" ],
    "ClientWhitelist": [ "dev-id-1", "dev-id-2" ],
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "1s",
        "Limit": 2
      },
      {
        "Endpoint": "*",
        "Period": "15m",
        "Limit": 100
      },
      {
        "Endpoint": "*",
        "Period": "12h",
        "Limit": 1000
      },
      {
        "Endpoint": "*",
        "Period": "7d",
        "Limit": 10000
      }
    ]
  }

設定說明

EnableEndpointRateLimiting 不同設定對規則引響不太一樣,假如設為true,所有規則 Rate Limit Count 是拆開的。false所有規則會加總算。

If EnableEndpointRateLimiting is set to false then the limits will apply globally and only the rules that have as endpoint * will apply. For example, if you set a limit of 5 calls per second, any HTTP call to any endpoint will count towards that limit.

If EnableEndpointRateLimiting is set to true, then the limits will apply for each endpoint as in {HTTP_Verb}{PATH}. For example if you set a limit of 5 calls per second for *:/api/values a client can call GET /api/values 5 times per second but also 5 times PUT /api/values.

StackBlockedRequests當API RateLimit出現,訪問中API要不要計算在內,通常是設定false

RealIpHeader通常反向代理後會有 IP 參考 Header,這邊可以針對情況調整,沒有 Header 他會抓 IP資料。

也有針對 IP 設定也可以設定,不過我是沒設定這段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 "IpRateLimitPolicies": {
    "IpRules": [
      {
        "Ip": "84.247.85.224",
        "Rules": [
          {
            "Endpoint": "*",
            "Period": "1s",
            "Limit": 10
          },
          {
            "Endpoint": "*",
            "Period": "15m",
            "Limit": 200
          }
        ]
      },
      {
        "Ip": "192.168.3.22/25",
        "Rules": [
          {
            "Endpoint": "*",
            "Period": "1s",
            "Limit": 5
          },
          {
            "Endpoint": "*",
            "Period": "15m",
            "Limit": 150
          },
          {
            "Endpoint": "*",
            "Period": "12h",
            "Limit": 500
          }
        ]
      }
    ]
  }

特定 API 設定 RateLimit

我專案只需要指定新增 Post API 不要一直呼叫,可以這樣設定。付款流程理論上8秒內只會觸發一次,這邊特別設定這樣。可以阻擋前端連點或不明原因呼叫。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "IpWhitelist": [],
    "ClientWhitelist": [],
    "GeneralRules": [
      {
        "Endpoint": "post:/api/Pay",
        "Period": "8s",
        "Limit": 1
      },
      {
        "Endpoint": "post:/api/newXXX",
        "Period": "1s",
        "Limit": 1
      }
    ]
  }

ClientLimit

當初看到 ClientId本來以為跟 OAuth 有關係,但其實沒有關係,他就是抓個Header上面的 X-Client。如果所有 Request 沒設定到就會把 X-Client 當作一樣 key。所以使用這個情境應該比較低,我也找不到 Reverse Proxy 設定 X-Client,不太了解這個使用時機。

使用方法跟 IpRateLimit 差不多,這邊就不完整顯示,請參考ClientRateLimitMiddleware · stefanprodan/AspNetCoreRateLimit Wiki

使用者驗證

其實 ClinetLimitIpRateLimit 實務上需求比較少,假如在同一個網域很多Web 做 Nat 情況下,使用者 IP 匯市一樣的,IpRateLimit會有問題。所以假如能做到身分驗證當 Key 就能解決這個問題。原生沒有這個方法,這邊找到幾個方式。

這邊整理一下找到方法。

方法1: 使用 Authorization Header(Workaround) 不推薦

使用 "ClientIdHeader": "Authorization",但這個缺點很明顯。使用這個 AccessToken 需要紀錄很長;還有第二個缺點,使用者換 Token 就不會阻擋。

參考:use jwt in rate limiting · Issue #37 · stefanprodan/AspNetCoreRateLimit

方法2: 設定 Request X-ClientId 為 User Identity

1.設定好相關Token-based 身分驗證理論上你的專案都有設定好,這邊就不說明。

  1. 在走 ClientRateLimmit前設定塞 X-ClientId。這邊可以額外拆 class,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.UseAuthorization();
app.Use(async(context, next)=>
{
    try
    {
        context.Request.Headers.Add("X-ClientId", context.User.Identity!.Name);
    }
    catch (Exception)
    {
        context.Request.Headers.Add("X-ClientId", "not authenticated"); 
    }
    await next();
});
app.UseHttpLogging();
app.UseClientRateLimiting();

RateLimit:Rate limit after authentication · Issue #82 · stefanprodan/AspNetCoreRateLimit

方法3: RateLimit進階設定

這個寫法相對方法2比較正規寫法,自己寫相關規則去判斷再注入DI裡面去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ElmahIoRateLimitConfiguration : RateLimitConfiguration
{
    public ElmahIoRateLimitConfiguration(
        IOptions<IpRateLimitOptions> ipOptions,
        IOptions<ClientRateLimitOptions> clientOptions)
            : base(ipOptions, clientOptions)
    {
    }

    public override void RegisterResolvers()
    {
        base.RegisterResolvers();
        ClientResolvers.Add(new ClientQueryStringResolveContributor());
    }
}

public class ClientQueryStringResolveContributor : IClientResolveContributor
{
    public Task<string> ResolveClientAsync(HttpContext httpContext)
    {
        var queryDictionary =
            Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(
                httpContext.Request.QueryString.ToString());
        if (queryDictionary.ContainsKey("api_key")
            && !string.IsNullOrWhiteSpace(queryDictionary["api_key"]))
        {
            return Task.FromResult(queryDictionary["api_key"].ToString());
        }

        return Task.FromResult(Guid.NewGuid().ToString());
    }
}

參考: Built-in rate limiting in ASP.NET Core vs AspNetCoreRateLimit

我這個範例不是 Token-based ,這邊有另外一種寫法。
use jwt in rate limiting · Issue #37 · stefanprodan/AspNetCoreRateLimit
不過使用沒有很順利,這邊文章(How to implement Rate Limiting in an ASP.NET Core Web API)照網路設定順利可以跑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
        // configuration (resolvers, counter key builders)
        builder.Services.AddSingleton<IRateLimitConfiguration, CustomRateLimitConfiguration>();


// 省略...

internal class CustomRateLimitConfiguration : RateLimitConfiguration
{
    public CustomRateLimitConfiguration(
        IOptions<IpRateLimitOptions> ipOptions, 
        IOptions<ClientRateLimitOptions> clientOptions) : base(ipOptions, clientOptions)
    {
    }

    public override void RegisterResolvers()
    {
        ClientResolvers.Add(new ClientIdResolverContributor());
    }
}

internal class ClientIdResolverContributor : IClientResolveContributor
{
    public Task<string> ResolveClientAsync(HttpContext context)
    {
        
        return Task.FromResult<string>(context.User.Identity.Name);
    }
}

彩蛋

nginx反向代理設定 Header