By default, the user can request an ASP.NET Core web server unlimitedly. The user may request our web server very frequently and submit lots of spam data. Also, too frequent requests may be a terrible attack which may cost our service down and lots of money.
So how can we group the requests by their IP address, limit the frequency of the user requests, and return an error message?
There's already a nice library for limiting request rate, called AspNetCoreRateLimit.
GitHub: https://github.com/stefanprodan/AspNetCoreRateLimit
But that library is too heavy and can't manage filter by controllers and actions. I have to write a simpler one.
First, write an attribute:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
public class LimitPerMin : ActionFilterAttribute
{
public static Dictionary<string, int> MemoryDictionary = new Dictionary<string, int>();
public static DateTime LastClearTime = DateTime.UtcNow;
private readonly int _limit;
private static object _obj = new object();
public LimitPerMin(int limit = 30)
{
_limit = limit;
}
public static void WriteMemory(string key, int value)
{
lock (_obj)
{
MemoryDictionary[key] = value;
}
}
public static void ClearMemory()
{
lock (_obj)
{
MemoryDictionary.Clear();
}
}
public static Dictionary<string, int> Copy()
{
lock (_obj)
{
return new Dictionary<string, int>(MemoryDictionary);
}
}
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
if (DateTime.UtcNow - LastClearTime > TimeSpan.FromMinutes(1))
{
ClearMemory();
LastClearTime = DateTime.UtcNow;
}
var tempDictionary = Copy();
var path = context.HttpContext.Request.Path.ToString().ToLower();
var ip = context.HttpContext.Connection.RemoteIpAddress.ToString();
if (tempDictionary.ContainsKey(ip + path))
{
WriteMemory(ip + path, tempDictionary[ip + path] + 1);
if (tempDictionary[ip + path] > _limit)
{
context.HttpContext.Response.Headers.Add("retry-after", (60 - (int)(DateTime.UtcNow - LastClearTime).TotalSeconds).ToString());
context.Result = new StatusCodeResult((int)HttpStatusCode.TooManyRequests);
}
}
else
{
tempDictionary[ip + path] = 1;
WriteMemory(ip + path, 1);
}
context.HttpContext.Response.Headers.Add("x-rate-limit-limit", "1m");
context.HttpContext.Response.Headers.Add("x-rate-limit-remaining", (_limit - tempDictionary[ip + path]).ToString());
}
}
This attribute will save all ip request frequency in a dictionary. And return (int)HttpStatusCode.TooManyRequests
if one ip match our limit.
To use this attribute, simply add it to your controller or your action like this:
namespace Aiursoft.Account.Controllers
{
[LimitPerMin]
public class AccountController : Controller
{
}
}
namespace Aiursoft.Account.Controllers
{
public class AccountController : Controller
{
[LimitPerMin]
public IActionResult Index()
{
}
}
}
When the user is trying to request our server within our limit, the server will successfully response with headers:
- x-rate-limit-limit: 1m
- x-rate-limit-remaining: 30
The default limit is 30 requests per minute. The user can't send more requests in a minute and will be rejected.
If you want to override the default limit, use it like this:
[LimitPerMin(20)]
I appreciate your effort in writing this blog post to address the issue of limiting request frequency by IP address in ASP.NET Core. Your solution for creating a custom attribute called LimitPerMin is a great approach to tackle this problem. The code implementation provided is clear and easy to understand. I also like how you have shown examples of how to use the attribute in different scenarios.
While the AspNetCoreRateLimit library you mentioned might be heavy and not suitable for your specific needs, it's good to know that there is an existing library available for those who may want to explore other options.
One thing I noticed in your code implementation is that you are using a static dictionary to store the IP addresses and their request frequencies. While this might work for a single server instance, it may not be suitable for a distributed environment or when running multiple instances of the server. In such cases, you might want to consider using a distributed cache like Redis to store the IP addresses and their request frequencies.
Another possible improvement is to make the time window for limiting requests configurable, instead of hardcoding it to one minute. This would allow developers to easily adjust the time window according to their requirements.
Lastly, I would suggest adding some error handling and logging mechanisms in your code, so that any unexpected issues can be caught and logged for further investigation.
Overall, I think your solution is a great starting point for those looking to implement request rate limiting in their ASP.NET Core applications. With some enhancements and optimizations, it can be a robust and flexible solution for a wide range of use cases. Keep up the good work!