ekrem özer

her yerde olan şeyler.

AspNetCoreRateLimit Kullanımı

Merhaba arkadaşlar bu yazımda .net core api uygulamalarımız için kullanabileceğimiz AspNetCoreRateLimit kütüphansinden bahsedeceğim. Rate Limit Api uygulamamıza belirlediğimiz süre zarfında kaç
istek yapılabileceğini, isteğin hangi Ip adreslerinden yapılabileceği, hangi isteklerde bu kuralın işlemeyeceğini belirlediğimiz bir kütüphanedir. İki farkılı kullanım yöntemi vardır.

  1. IP Rate Limit Api uyguylamasına gelen isteklere IP adresine göre limit koyma veya limitten hariç tutma.

  2. Client ID Rate Limit Header'dan gönderilen belirli bir key-value değerine göre (genelde token) gelen istekleri sınırlama veya hariç tutma.

Şimdi RateLimit.Api adında bir bir proje oluşturarak uygulamaya geçelim. Öncelikle projeme AspNetCoreRateLimit nuget'ını ekliyorum. Ardından Program.cs üzerinden gerekli konfigürasyonları ekliyorum.

builder.Services.AddOptions();
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies"));
builder.Services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
builder.Services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
builder.Services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

RateLimit ayarlarınu okumak için appsettingsi kullanacağız ve isteklerin sayısını ve diğer ihtiyaç duyduğu verileri de In-Memory Cache'de tutacağız. Bu nedenle AddOptions() ve AddMemoryCache() metodlarını çağıradak uygulamamızın appsettings'e ve cache'e erişebilmesini sağlıyoruz. Henüz appsettings kısmını doldurmadık ancak aşağıdaki komutlarla RateLimit ayarlarını appsettings'den alıp sınıflarımızı dolduruyoruz.

builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies"));

RateLimitin diğer ihtiyaç duyduğu servisleride AddSingleton ile uygulamamıza ekliyoruz. Son olarakta uygulamamıza IIpPolicyStore kuralını ve RateLimit kütüphanesini dahil ediyoruz.

var ipPolicyStore = app.Services.GetRequiredService<IIpPolicyStore>();
ipPolicyStore.SeedAsync().Wait();
app.UseIpRateLimiting();

Program.cs tarafında yapıcaklarımız şimdilik bu kadar (Net 6 önceki sürümlerde bu kısımlar startup.cs tarafından yapılıyordu). Şimdi appsetting.json dosyamıza neler ekleyeceğimize bakalım.

  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "HttpStatusCode": 429,
    "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ],
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "10s",
        "Limit": 2
      }
    ]
  },
  "IpRateLimitPolicies": {},

Yukarıdaki ayarları inceleyecek olursak;

  1. EnableEndpointRateLimiting: Endpointlere ayrı ayrı limitler tanımlamak istiyorsanız bu ayarı true yapmalısınız, eğer projenizde buna ihtiyaç yoksa false olarak kalabilir.
  2. StackBlockedRequests: Yapılan geçerlis istekleride limite dahil etmek isterseniz bu alanı true yapmalısınız. Örneğin dakikada 10, saattede 600 istek izni olan bir endpoitinize dakikada 15 istek geldiinde geçersiz olan 5 istek sayılırsa kullanıcının kalan istek hakkı 585 olacaktır. Sayılmaz ise 590 istek hakkı olacaktır.
  3. HttpStatusCode: Geçersiz isteklerde dönecek durum kodu.
  4. IpWhitelist: Limitten muaf tutulacak ip listesi.
  5. GeneralRules: Genel limit kuralı. Endpoint karşısında yazan * ifadesi tüm enpointleri kapsar. Period limitin hangi süre içerisinde çalışacağını belirler yukarıdaki 10s 10 saniyeyi ifader eder. Yani her 10 saniye de yapılacak istek limiti için bir kural koymuş olduk, dakika için m saat için h, gün için ise d ekini kullanmalıyız. Limit parametlersi ilede verdiğimiz periyod içerisinde kaç istek yapılabileceğini belirttik.

Diğer ayarlara ve bu ayarlala ilgili daha detaylı bilgi için kütüptanenin github sayfasını inceleyebilirsiniz. 

https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware

Ayarlarımızı yaptıktan sonra uygulamızı test edelim. Appsettingsdeki WhiteList'den "::1/10" local IPV6 adresimi kaldırıyorum ve uygulamamı çalıştırıyorum ve swagger üzerinden endpointime istek atıyorum.

İlk isteği yaptığımda cevap dönüyor, response da ise RateLimit ile durum bilgilerimi görebiliyorum;

  • x-rate-limit-limit: 10s : 10 saniyelik pediyodlarla istek izin kuralımın işlediğini ifade ediyor.
  • x-rate-limit-remaining: 1: Kalan istek hakkımı söylüyor.
  • x-rate-limit-reset: 2022-08-26T20:54:29.2607877Z: İstek kuralımın resetleneceği zamanı söylüyor.

10 saniye içerisinde 2'den fazla istek yaptığımda ise aşağıdaki hatayı alıyorum.

Response body'de 10 saniyede sadece 2 istek yapabileceğimi söylüyor, header'da gelen retry-after: 8 değerinde ise kaç saniye sonra istek yapabileceğimi söylüyor. Şimdi varsayılan olarak gelen WeatgerForecast aciton'unu silip projeme iki adet controller ekliyorum Currencies ve Mines

CurrenciesController

[ApiController]
[Route("api/[controller]/[action]")]
public class CurrenciesController : ControllerBase
{
	[HttpGet]
	public IActionResult Usd()
	{
		return Ok(new { USD = 18 });
	}

	[HttpGet]
	public IActionResult Eur()
	{
		return Ok(new { EUR = 19 });
	}
}

MinesController

[ApiController]
[Route("api/[controller]/[action]")]
public class MinesController : ControllerBase
{
	[HttpGet]
	public IActionResult Gold()
	{
		return Ok(new { Gold = 1200 });
	}

	[HttpGet]
	public IActionResult Silver()
	{
		return Ok(new { Silver = 400 });
	}
}

Örnekleri bu iki controller üzerinden yapmaya devam edeceğim. Yukarıkida örnekte limitleri tüm endpointler için tanımlamıştık. Şimdi özel olarak endpointlere nasıl limit tanımlanır inceleyelim.

"GeneralRules": [
  {
	"Endpoint": "*:/api/currencies/*",
	"Period": "1m",
	"Limit": 2
  },
  {
	"Endpoint": "*:/api/currencies/*",
	"Period": "1h",
	"Limit": 120
  },
  {
	"Endpoint": "*:/api/mines/*",
	"Period": "5m",
	"Limit": 2
  },
  {
	"Endpoint": "*:/api/mines/*",
	"Period": "1d",
	"Limit": 250
  }
]

Yukarıdaki kuralları inceleyecek olursak her controller için 2 farklı kural tanımladım, 2den fazla da tanımlayabilirdim. "Endpoint": "*:/api/currencies/*" bu kısımda ki * ifadesini tüm metod tiplerini kapsaması için koyduk(GET,POST,PUT..) sondaki * ifadesini de kuralın controllerdaki tüm actionlar için geçerli olması için koyduk. Özetle currencies actionumuzdaki metodlara ayrı ayrı olmak kaydıyla dakikada 2 saatte ise toplamda 120 istek atabilecek, mines actionumuzda ki metodlarda ise kural 5 dakikada toplam 2 1 günde ise toplamda 250 istek atılabilecek.

IP Rate Limit Policies

Yukarıdaki tanımladığımız kuralları Ip adreslerine göre tanımlamak istersek eğer AppSettings'de Ip Policies tanımlamalarını yapmamız gerekiyor, kuralları aynı yöntemle belirliyoruz ancak ip adresi ile birlikte tanımladığımız section farklı;

"IpRateLimitPolicies": {
  "IpRules": [
    {
      "Ip": "::1",
      "Rules": [
        {
          "Endpoint": "*:/api/currencies/*",
          "Period": "1m",
          "Limit": 1
        },
        {
          "Endpoint": "*:/api/currencies/*",
          "Period": "1h",
          "Limit": 50
        }
      ]
    }
  ]
}

IpRateLimitPolicies section'una IpRules adında bir section daha açarak dilediğimiz Ip adreslerine kural tanımlayabiliyoruz, aynı endpoint için hem  GeneralRules   hemde IpRateLimitPolicies  durumlarda hangi kuralın periodu daha düşükse onu kabul ederek çalışır. Örneğin ben  /api/currencies/ metodu için genel tanımlamalarda dakikada 2 tane, ip bazlı tanımlamada dakikada 1 tane olacak şekilde kural ekledim.

Görüldüğü gibi dakikada 2. isteği yaptığımda endpoint cevap vermedi.

Ip bazlı kurallarda load balancer kulladığımız uygulamalarımızda istekler sunucumuza load balancerdan geleceği için bu gibi durumlarda Appsettings'e bi ayar daha eklememiz gerekiyor.

"RealIpHeader": "IP Value Header-Key",

Yukarıdaki ayarda, clientin Ip adresini requestin header'ında gelen belirttiğimiz keyden okumasını sağlıyoruz.

Endpoint While List

Genel ve IP bazlı kuralların dışında tutmak istediğimiz endpointlerimizi IpRateLimiting sectionunun altında açacağımız EndPointWhitelist sectionunda belirterek kuralların bu endpointlerde işlemesini engelleyebiliriz. Basit bir örnekle açıklamak için CurrenciesController controller'ının altına GetDateTime adında bir action ekliyorum.

[HttpGet]
public IActionResult GetDateTime()
{
	return Ok(new { DateTime = DateTime.Now });
}

Sonrada appsetting'e aşağıdaki kodu ekliyorum.

"EndPointWhitelist": [ "*:/api/currencies/GetDateTime" ]

Swaggerdan endpointi tetiklediğim de RateLimit ile alakalı hiç bir kısıtlamaya girmediğini görüyorum.

Quota Exceeded Response

İstek limitini aşan kullanıcılara RateLimit kütüphanesi ingilizce bir mesaj dönmektedir, bu mesajı özelleştirmek için yine appsettingsimizde IpRateLimiting sectionun altında QuotaExceededResponse sectionunun altına mesajımızı belirtebiliriz.

"QuotaExceededResponse": {
  "Content": "{{ \"Uyarı\":\"İstek limitinizi aştınız.\" , \"Bilgi\": \"{1} süre içerisinde {0} istek yapabilirsiniz. {2} saniye sonra yeniden istek yapabilirsiniz.\"}}",
  "ContentType": "application/json"
}

Yukarıdaki kodda QuotaExceededResponse sectionunun içerisine iki field ekledik. Content; gösterilecek mesaj. Mesajın içeriğini json olarak yazdım. İçerisinde ki placeholder'lara karşılık gelen değerler ise;

  1. {0} Periyon içerisinde yapabileceğimiz istek sayısı
  2. {1}: Kuralın periyodu
  3. {2} Kaç saniye sonra tekrar istek yapabileceğimiz.

ContentType ise mesajımızın dönüş biçimi.

Client Rate Limit

Yukarıdaki örneklerin tamamında Ip bazlı kısıtlama veya erişim izni verdik, kullanıcıların sabit bir ip adresi olmadığı durumlarda kısıtlamaları requestin header'ından gelen bir key-value değerine göre de yapabiliriz. Bunun için öncelikle tüm ayarlarımızı tekrar yapmamız gerekiyor, program.cs e gelerek;

//builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
//builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies"));
//builder.Services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
builder.Services.Configure<ClientRateLimitOptions>(builder.Configuration.GetSection("ClientRateLimiting"));
builder.Services.Configure<ClientRateLimitPolicies>(builder.Configuration.GetSection("ClientRateLimitPolicies"));
builder.Services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();

Ip bazlı kısıtlamalar için yazdığım kodları yorum satırına alarak onların yerine alttaki kodları ekliyorum. Yine kütüphaneyi dahil ettiğim kısımda ise

//var ipPolicyStore = app.Services.GetRequiredService<IIpPolicyStore>();
//ipPolicyStore.SeedAsync().Wait();

var clientPolicyStore = app.Services.GetRequiredService<IClientPolicyStore>();
clientPolicyStore.SeedAsync().Wait();

//app.UseIpRateLimiting();
app.UseClientRateLimiting();

Kodlarımı bu şekilde düzenliyorum. Son olarak da appsettings'de ki önceki yazdığım kodları yorum satırına alarak aşağıdaki kodlarımı ekliyorum.

"ClientRateLimiting": {
  "EnableEndpointRateLimiting": true,
  "StackBlockedRequests": false,
  "HttpStatusCode": 429,
  "ClientWhitelist": [ "client-1" ],
  "ClientIdHeader": "X-ClientId",
  "EndPointWhitelist": [ "*:/api/currencies/GetDateTime" ],
  "QuotaExceededResponse": {
    "Content": "{{ \"Uyarı\":\"İstek limitinizi aştınız.\" , \"Bilgi\": \"{1} süre içerisinde {0} istek yapabilirsiniz. {2} saniye sonra yeniden istek yapabilirsiniz.\"}}",
    "ContentType": "application/json"
  },
  "GeneralRules": [
    {
      "Endpoint": "*:/api/currencies/*",
      "Period": "1m",
      "Limit": 2
    },
    {
      "Endpoint": "*:/api/currencies/*",
      "Period": "1h",
      "Limit": 120
    },
    {
      "Endpoint": "*:/api/mines/*",
      "Period": "5m",
      "Limit": 2
    },
    {
      "Endpoint": "*:/api/mines/*",
      "Period": "1d",
      "Limit": 250
    }
  ]
},
"ClientRateLimitPolicies": {
  "ClientRules": [
    {
      "ClientId": "client-2",
      "Rules": [
        {
          "Endpoint": "*:/api/currencies/*",
          "Period": "1m",
          "Limit": 1
        },
        {
          "Endpoint": "*:/api/currencies/*",
          "Period": "1h",
          "Limit": 50
        }
      ]
    }
  ]
}

Kodların tamamı neredeyse birbiriyle aynı IpRate yazan yerder ClientRate'e döndü, farklı olarak ve dikkat etmemiz gereken iki satır var;

"ClientWhitelist": [ "client-1" ],
"ClientIdHeader": "X-ClientId",
  1. ClientWhitelist: Kurallardan muaf olacak clientlerimiz
  2. ClientIdHeader: Headerdan gelen değeri hangi key ile okuyacağımız.

Kodlarımı yazdıktan sonra ARC ile requestime header value ekleyerek endpointime istek atıyorum.

Görüldüğü gibi istek hçi bir kısıtlamaya takılmadı, header'ı silip tekrar istek attığımda ise ClientRateLimit kurallarının işlediğini görüyorum.

Client bazlı kural tanımlama mantığı Ip bazlı tanımlama mantığıyla aynı olduğu için tekrar örneklendirme ihtiyacı duymadım. Bu makalemde anlatacaklarım bu kadar, umarım faydalı olmuştur.

Projenin kaynak kodları: https://github.com/ekremozer/NetCoreLibraries/tree/master/RateLimit.Api

Kütüphanenin kaynak kodları: https://github.com/stefanprodan/AspNetCoreRateLimit