ASP.NET Core 添加API限流
前言
最近發(fā)現(xiàn)有客戶在大量的請(qǐng)求我們的接口,出于性能考慮遂添加了請(qǐng)求頻率限制。
由于我們接口請(qǐng)求的是.Net Core寫的API網(wǎng)關(guān),所以可以直接添加一個(gè)中間件,中間件中使用請(qǐng)求的地址當(dāng)key,通過配置中心讀取對(duì)應(yīng)的請(qǐng)求頻率參數(shù)設(shè)置,然后通過設(shè)置redis的過期時(shí)間就能實(shí)現(xiàn)了。
添加一個(gè)中間件ApiThrottleMiddleware,使用httpContext.Request.Path獲取請(qǐng)求的接口,然后以次為key去讀取配置中心設(shè)置的請(qǐng)求頻率設(shè)置。(Ps:使用_configuration.GetSection(apiUrl).Get
1 public class ApiThrottleMiddleware
2 {
3 private readonly RequestDelegate _next;
4 private IConfiguration _configuration;
5 private readonly IRedisRunConfigDatabaseProvider _redisRunConfigDatabaseProvider;
6 private readonly IDatabase _database;
7
8 public ApiThrottleMiddleware(RequestDelegate next,
9 IConfiguration configuration,
10 IRedisRunConfigDatabaseProvider redisRunConfigDatabaseProvider)
11 {
12 _next = next;
13 _configuration = configuration;
14 _redisRunConfigDatabaseProvider = redisRunConfigDatabaseProvider;
15 _database = _redisRunConfigDatabaseProvider.GetDatabase();
16 }
17
18 public async Task Invoke(HttpContext httpContext)
19 {
20 var middlewareContext = httpContext.GetOrCreateMiddlewareContext();
21 var apiUrl = httpContext.Request.Path.ToString();
22
23 var jsonValue= _configuration.GetSection(apiUrl).Value;
24 var apiThrottleConfig=JsonConvert.DeserializeObject<ApiThrottleConfig>(jsonValue);
25 //var apiThrottleConfig = _configuration.GetSection(apiUrl).Get<ApiThrottleConfig>();
26
27 await _next.Invoke(httpContext);
28 }
29 }
我們使用的配置中心是Apollo,設(shè)置的格式如下,其中Duration為請(qǐng)求間隔/秒,Limit為調(diào)用次數(shù)。(下圖設(shè)置為每分鐘允許請(qǐng)求10次)

(Ps:由于在API限流中間件前我們已經(jīng)通過了一個(gè)接口簽名驗(yàn)證的中間件了,所以我們可以拿到調(diào)用客戶的具體信息)
如果請(qǐng)求地址沒有配置請(qǐng)求頻率控制,則直接跳過。
否則先通過SortedSetLengthAsync獲取對(duì)應(yīng)key的記錄數(shù),其中key我們使用了 $"{客戶Id}:{插件編碼}:{請(qǐng)求地址}",以此來限制每個(gè)客戶,每個(gè)插件對(duì)應(yīng)的某個(gè)接口來控制請(qǐng)求頻率。獲取key對(duì)應(yīng)集合,當(dāng)前時(shí)間-配置的時(shí)間段到當(dāng)前時(shí)間的記錄。
1 /// <summary>
2 /// 獲取key
3 /// </summary>
4 /// <param name="signInfo"></param>
5 /// <param name="apiUrl">接口地址</param>
6 /// <returns></returns>
7 private string GetApiRecordKey(InterfaceSignInfo signInfo,string apiUrl)
8 {
9 var key = $"{signInfo.LicNo}:{signInfo.PluginCode}:{apiUrl}";
10 return key;
11 }
12
13 /// <summary>
14 /// 獲取接口調(diào)用次數(shù)
15 /// </summary>
16 /// <param name="signInfo"></param>
17 /// <param name="apiUrl">接口地址</param>
18 /// <param name="duration">超時(shí)時(shí)間</param>
19 /// <returns></returns>
20 public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration)
21 {
22 var key = GetApiRecordKey(signInfo, apiUrl);
23 var nowTicks = DateTime.Now.Ticks;
24 return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks);
25 }
如果請(qǐng)求次數(shù)大于等于我們?cè)O(shè)置的頻率就直接返回接口調(diào)用頻率超過限制錯(cuò)誤,否則則在key對(duì)應(yīng)的集合中添加一條記錄,同時(shí)將對(duì)應(yīng)key的過期時(shí)間設(shè)置為我們配置的限制時(shí)間。
/// <summary>
/// 獲取接口調(diào)用次數(shù)
/// </summary>
/// <param name="signInfo"></param>
/// <param name="apiUrl">接口地址</param>
/// <param name="duration">超時(shí)時(shí)間</param>
/// <returns></returns>
public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration)
{
var key = GetApiRecordKey(signInfo, apiUrl);
var nowTicks = DateTime.Now.Ticks;
return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks);
}
然后只需要在Startup中,在API簽名驗(yàn)證中間件后調(diào)用我們這個(gè)API限流中間件就行了。
以下為完整的代碼
using ApiGateway.Core.Configuration;
using ApiGateway.Core.Domain.Authentication;
using ApiGateway.Core.Domain.Configuration;
using ApiGateway.Core.Domain.Errors;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
namespace ApiGateway.Core.Middleware.Api
{
/// <summary>
/// API限流中間件
/// </summary>
public class ApiThrottleMiddleware
{
private readonly RequestDelegate _next;
private IConfiguration _configuration;
private readonly IRedisRunConfigDatabaseProvider _redisRunConfigDatabaseProvider;
private readonly IDatabase _database;
public ApiThrottleMiddleware(RequestDelegate next,
IConfiguration configuration,
IRedisRunConfigDatabaseProvider redisRunConfigDatabaseProvider)
{
_next = next;
_configuration = configuration;
_redisRunConfigDatabaseProvider = redisRunConfigDatabaseProvider;
_database = _redisRunConfigDatabaseProvider.GetDatabase();
}
public async Task Invoke(HttpContext httpContext)
{
var middlewareContext = httpContext.GetOrCreateMiddlewareContext();
var apiUrl = httpContext.Request.Path.ToString();
var jsonValue= _configuration.GetSection(apiUrl).Value;
if (!string.IsNullOrEmpty(jsonValue))
{
var apiThrottleConfig = JsonConvert.DeserializeObject<ApiThrottleConfig>(jsonValue);
//var apiThrottleConfig = _configuration.GetSection(apiUrl).Get<ApiThrottleConfig>();
var count = await GetApiRecordCountAsync(middlewareContext.InterfaceSignInfo, apiUrl, apiThrottleConfig.Duration);
if (count >= apiThrottleConfig.Limit)
{
middlewareContext.Errors.Add(new Error("接口調(diào)用頻率超過限制", GatewayErrorCode.OverThrottleError));
}
else
{
await AddApiRecordCountAsync(middlewareContext.InterfaceSignInfo, apiUrl, apiThrottleConfig.Duration);
}
}
await _next.Invoke(httpContext);
}
/// <summary>
/// 獲取接口調(diào)用次數(shù)
/// </summary>
/// <param name="signInfo"></param>
/// <param name="apiUrl">接口地址</param>
/// <param name="duration">超時(shí)時(shí)間</param>
/// <returns></returns>
public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration)
{
var key = GetApiRecordKey(signInfo, apiUrl);
var nowTicks = DateTime.Now.Ticks;
return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks);
}
/// <summary>
/// 添加調(diào)用次數(shù)
/// </summary>
/// <param name="signInfo"></param>
/// <param name="apiUrl">接口地址</param>
/// <param name="duration">超時(shí)時(shí)間</param>
/// <returns></returns>
public async Task AddApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration)
{
var key = GetApiRecordKey(signInfo, apiUrl);
var nowTicks = DateTime.Now.Ticks;
await _database.SortedSetAddAsync(key, nowTicks.ToString(), nowTicks);
await _database.KeyExpireAsync(key, TimeSpan.FromSeconds(duration));
}
/// <summary>
/// 獲取key
/// </summary>
/// <param name="signInfo"></param>
/// <param name="apiUrl">接口地址</param>
/// <returns></returns>
private string GetApiRecordKey(InterfaceSignInfo signInfo,string apiUrl)
{
var key = $"_api_throttle:{signInfo.LicNo}:{signInfo.PluginCode}:{apiUrl}";
return key;
}
}
}
轉(zhuǎn)自:Cyril
鏈接:cnblogs.com/Cyril-hcj/p/15136026.html
