速率限制允许我们限制某人在设定的时间内可以访问端点的次数。
这样做通常有两个原因:
- 阻止人们向我们的 API 发送垃圾邮件调用,并减慢系统速度或给我们带来很多钱(如果我们打开了自动缩放)。
-它还使我们能够向客户收取更高的费率限制。
在这里,我们将研究速率限制中间件,这将允许我们为不同的端点设置不同的策略,并为不同的客户设置限制。
首先,我们需要一个可以放置在端点上的属性,以指示我们想要对其进行速率限制并为其设置必要的策略。
public class RateLimiterAttribute : ActionFilterAttribute
{
public string Plan { get; set; }
public RateLimiterAttribute()
{
}
public RateLimiterAttribute(string plan)
{
Plan = plan;
}
}
现在,我们必须弄清楚如何将每个请求与提出请求的客户相关联。最常见的方法是在请求头中传递 apiKey 或客户 ID。不过,由于任何人都可以为该标头想到不同的名称,因此最简单的方法是拥有一个 IKeyExtractor intercaste,并让任何人根据需要实现它。
这是我的版本:
public class KeyExtractor : IKeyExtractor
{
public string ExtractKey(HttpContext context)
{
var header = context.Request.Headers["x-api-key"\];
return header.FirstOrDefault();
}
}
下一步是获取计划定义。我们需要每个速率限制计划的默认值,以及允许某些客户具有不同值的方法。
我们的速率限制计划定义如下所示:
public class RateLimitPlan
{
public string Name { get; set; }
public string Key { get; set; }
public int CallsNumber{ get; set; }
}
每个计划都有一个名称,我们将将其放入速率限制属性中,一个键可以是客户 ID/API 密钥或 * 作为默认值,以及 CallNumber,它设置我们允许的调用次数。
为了获取计划,我们执行以下操作:
public Task<RateLimitPlan> GetPlan(string planName, string key)
{
//get from the DB
var cachedPlan = _storage.GetPlan(planName, key);
if (cachedPlan != null)
{
return Task.FromResult(cachedPlan);
}
var allPlans = PlanList();
var plans = allPlans.Where(p => p.Name == planName &&
(p.Key == key || p.Key == "*")).ToList();
var plan = plans.Count == 1
? plans.First()//no specific plan for this key, just the default plan
: plans.First(p => p.Key == key);
_storage.SavePlan(planName, key, plan);
return Task.FromResult(plan);
}
我们获取计划名称和调用密钥(客户 ID/API 密钥)。首先,我们查看我们的缓存,看看我们是否已经有这个键的这个计划(因为我们对每个调用都这样做,我们希望在数据库调用上节省)
如果我们的缓存中没有它,我们在数据库中搜索具有此计划名称的记录,键是请求键或*。
如果我们只有一条记录,则该计划只有默认设置,因此我们返回它。如果我们有多个记录,则请求键具有自定义值,我们必须返回它。
我们应该在返回此值之前将此值保存到缓存中。
现在我们有了计划,我们需要知道我们的请求密钥在当前一分钟内已经进行了多少次调用。
private static DateTime GetStartOfMinute()
{
var startOfMinute =
new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
DateTime.Now.Hour, DateTime.Now.Minute, 0);
return startOfMinute;
}
public Task<int> GetCurrentCount(string planName, string key)
{
var startOfMinute = GetStartOfMinute();
var count = _storage.GetCount(planName + "_" + key, startOfMinute);
return Task.FromResult(count);
}
.....
//the cache storate that save the count
public class RateLimitCacheMemoryStorage
{
public int GetCount(string key, DateTime dateTime)
{
key = $"{key}_{dateTime.Ticks}";
var hasValue = _cache.TryGetValue(key, out var count);
return hasValue ? (int)count : 0;
}
}
我创建了一个帮助方法,该方法返回一个 DateTime 对象,该对象表示当前分钟的开始(基本上,它是 DateTime.Now,秒数设置为 0)。
为了设置缓存键,我们将计划名称、请求键和分钟开始值组合在一起。
如果我们的缓存中没有此缓存键的任何内容,则返回 0;
我们还需要一种方法来更新计数。
public Task UpdateCount(string planName, string key, int count)
{
var startOfMinute = GetStartOfMinute();
_storage.UpdateCount(planName + "_" + key, startOfMinute, count);
return Task.CompletedTask;
}
....
//the cache storate that save the count
public class RateLimitCacheMemoryStorage
{
public void UpdateCount(string key, DateTime dateTime, int count )
{
key = $"{key}_{dateTime.Ticks}";
_cache.Set(key, count, absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(5));
}
}
我们需要的最后一部分是一个中间件来连接一切:
public class RateLimiterMiddleware(RequestDelegate next,
IRateLimitStorage storage,
IKeyExtractor keyExtractor)
{
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
var rateLimiterAttribute = endpoint?.Metadata.
GetMetadata<RateLimiterAttribute>();
var tooManyRequests = false;
if (rateLimiterAttribute != null)
{
var key = keyExtractor.ExtractKey(context);
var plan = await storage.GetPlan(rateLimiterAttribute.Plan, key);
var currentCount = await storage.GetCurrentCount(plan.Name, key);
if (currentCount >= plan.CallsNumber)
{
tooManyRequests = true;
}
else
{
await storage.UpdateCount(plan.Name, key, currentCount + 1);
}
}
if (tooManyRequests)
{
await HandleExceptionAsync(context);
}
else
{
await next(context);
}
}
private Task HandleExceptionAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = "application/json";
httpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
return httpContext.Response.WriteAsync("Too Many Requests");
}
}
对于每个请求,中间件都会检查端点是否具有 RateLimiter 属性。
如果是这样,我们将使用前面显示的方法来获取请求密钥、此请求密钥的此终结点的计划以及此请求密钥的当前调用计数。
然后,我们可以检查此调用是否超过计划中定义的调用次数;如果是这样,我们将返回一个异常,以向客户返回 429 TooManyRequests 错误。否则,我们将更新计数并允许请求发生。
源代码获取:公众号回复消息【code:11382
】