前言
我们在日常使用一些公有接口或者访问一些资源类网站的时候,经常会遇到如本文标题所示的一些提示,当前其目的也很明确,就是为了保证能有更多人更广泛的访问到资源,而对每个人的访问做出了一些限制。如近期大火的 ChatGPT,在申请 key 的时候也会提示每秒钟或者每分钟的访问次数。
那么这类功能,要怎么集成到自己的系统里?
本文讨论一下在.net core 开发环境下,实现访问限流的功能。
中间件
在.net 生态里,用于限流的工具有很多,尤其在.net 7 发布以后,原生支持了限流中间件(官方博客👉:https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/)但这个中间件的功能还不是很完善,所以需要一个更强大的中间件来帮我们实现限流功能。目前来说,这方面最好的选择就是 AspNetCoreRateLimit 这个中间件了,文档地址👉:https://github.com/stefanprodan/AspNetCoreRateLimit/wiki
具体的介绍,大家可以直奔官网查看,我这里就直接介绍应用案例了。
应用
这里,我主要从以下几个方面介绍接入案例
引入中间件
配置中间件,包括基于 Redis 的一些配置
基于 IP 地址限流
基于 ClientID 限流
1.引入中间件
这里,如果只是实现限流功能,引入第一个包就可以了,但由于当前的开发的系统,大部分都是支持分布式部署,所以在配置限流中间件时,要考虑分布式的情况,这时就需要用到第二个包。
2.配置中间
安装好以上两个包以后,我们需要进行一些配置,这里建议在项目里单拎出一个类文件进行配置,对比官方的配置,我稍微调整了一下,这里我就直接上代码了
public static IServiceCollection ConfigureRateLimit(this IServiceCollection services, IConfiguration configuration)
{
if (services == null) throw new ArgumentNullException(nameof(services));
//配置存储速率限制计数器和ip规则的方式,默认是存于内存,这是官方默认的存储方式
//services.AddMemoryCache();
//这里采用分布式存储方式
services.AddDistributedMemoryCache();
//这里我两种限流方式都配置了,需要根据不同的场景进行切换
//实际应用可以任选其一或者都选
// 从配置文件加载ip限流的配置
services.Configure<IpRateLimitOptions>(configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(configuration.GetSection("IpRateLimitPolicies"));
// 从配置文件加载客户端(clientId)限流的配置
services.Configure<ClientRateLimitOptions>(configuration.GetSection("ClientRateLimiting"));
services.Configure<ClientRateLimitPolicies>(configuration.GetSection("ClientRateLimitPolicies"));
//配置redis,我这里的配置比较多是因为redis采用了哨兵模式,如果是单redis节点
//不用这么麻烦,具体的配置可以参考stackexchange的官方文档
services.AddSingleton<IConnectionMultiplexer>(provider => {
ConfigurationOptions sentinelOptions = new ConfigurationOptions();
string[] sentinels = configuration.GetSection("RedisOption:SentinelNodes").Value.Split(',');
foreach (string sentinel in sentinels)
{
sentinelOptions.EndPoints.Add(sentinel);
}
sentinelOptions.TieBreaker = "";
sentinelOptions.CommandMap = CommandMap.Sentinel;
sentinelOptions.AbortOnConnectFail = false;
ConnectionMultiplexer sentinelConnection = ConnectionMultiplexer.Connect(sentinelOptions);
//通过配置文件,需按照官方给出的配置格式 https://stackexchange.github.io/StackExchange.Redis/Configuration
ConfigurationOptions redisServiceOptions = ConfigurationOptions.Parse(configuration.GetSection("RedisOption:SentinelService").Value);
ConnectionMultiplexer masterConnection = sentinelConnection.GetSentinelMasterConnection(redisServiceOptions);
return masterConnection;
});
//注入基于内存存储规则的先流方式(默认)
//services.AddInMemoryRateLimiting();
//注入基于redis存储规则限流方式
services.AddRedisRateLimiting();
//注入计数器和规则分布式高速缓存存储
services.AddSingleton<IIpPolicyStore, DistributedCacheIpPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();
//配置(解析器、计数器密钥生成器)
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
return services;
}
复制代码
这里有以下几个点需要注意以下
具体的注释,在代码里了。而涉及到 redis 的相关配置如下
"RedisOption": {
"Mode": "sentinel",
"SentinelNodes": "具体节点,也可以搞成数组,我这里就是一个串,逗号隔开了",
"SentinelService": "serviceName=[你的服务名],password=xxxx,defaultDatabase=[你的默认库],abortConnect=true"
},
复制代码
配置好服务之后,就可以注入中间件了,这里可以直接到 startup.cs 或者 program.cs(.net 6 以后就默认取消了 startup.cs)去注入,也可以像配置服务一样,单拎出来。这里因为我配置了两种方式,所以就单拎出来了
private static readonly ILogger _logger = new LoggerConfiguration().CreateLogger();
//基于ip地址限流
public static IApplicationBuilder UseIpLimit(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
try
{
return app.UseIpRateLimiting();
}
catch (Exception e)
{
_logger.Error($"基于IP限流时发生错误.\n{e.Message}");
}
return null;
}
//基于clientId限流
public static IApplicationBuilder UseClientLimit(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
try
{
return app.UseClientRateLimiting();
}
catch (Exception e)
{
_logger.Error($"基于客户端限流时发生错误.\n{e.Message}");
}
return null;
}
复制代码
然后注入中间件
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...其他服务
app.UseStaticFiles();
//注入ip限流中间件(考试系统有可能在机房进行,IP限制可能会有问题)
//app.UseIpLimit();
//注入客户端限流中间件(需要自己在请求客户端实现X-ClientId)
app.UseClientRateLimiting();
//...其他服务
}
复制代码
因为中间件的注入顺序是十分重要的,所以注入限流中间件时要注意不要太靠前,官方的建议时尽量写道外围,我这里是注入到了静态文件中间件后面,避免静态文件也算如到限流次数里,当然这个还是根据应用场景来调整的。接下来就是分别根据 ip 和 clientid 进行具体配置了,由于配置信息默认是读取的 appsetting.json,而限流的相关配置信息又相对复杂,为了保持配置文件的整洁,我们可以选择把限流的配置也单拎到一个单独的配置文件里,然后再 program.cs 里加载上这个配置,下面代码第 5-7 行就是这个做用,我把独立的配置文件统一放到了 Settings 目录里
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().ConfigureAppConfiguration((host, config) =>
{
config.AddJsonFile($"Settings/RateLimitConfig.json", optional: true, reloadOnChange: true);
});
webBuilder.ConfigureLogging(builder =>
{
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = false;
options.TimestampFormat = "[yyyy-MM-dd HH:mm:ss] ";
});
});
})
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
;
}
复制代码
到此,就可以去编写具体的配置文件了
3.基于 IP 地址的限流配置
这个部分直接看文档的具体解释👉:https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#setup我这里结合官方案例,再贴几个测试的效果
"IpRateLimiting": {
//"例如设置了5次每分钟访问限流。当False时:项目中每个接口都加入计数,不管你访问哪个接口,只要在一分钟内累计够5次,将禁止访问。": null,
//True:当一分钟请求了5次GetData接口,则该接口将在时间段内禁止访问,但是还可以访问PostData()5次,总得来说是每个接口都有5次在这一分钟,互不干扰。
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
//"IpWhitelist": [ "127.0.0.1", "::1", "192.168.0.0/24" ],
"IpWhitelist": ["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
}
],
"QuotaExceededResponse": {
"Content": "{{\"code\":429, \"msg\": \"您的访问过于频繁,已经触发限流规则,每{1}允许访问{0}次,请{2}后在试\", \"data\": \"Quota exceeded. Maximum allowed: {0} per {1}. Please try again in {2} second(s).\" }}",
"ContentType": "application/json",
//"StatusCode": 429
"StatusCode": 200
}
},
"IpRateLimitPolicies": {
"IpRules": [
{
"Ip": "84.247.85.224",
"Rules": [
{
"Endpoint": "*",
"Period": "1s",
"Limit": 10
},
{
"Endpoint": "*",
"Period": "15m",
"Limit": 200
}
]
},
{
"Ip": "192.168.0.46/25",
"Rules": [
{
"Endpoint": "*",
"Period": "1s",
"Limit": 5
},
{
"Endpoint": "*",
"Period": "15m",
"Limit": 150
},
{
"Endpoint": "*",
"Period": "12h",
"Limit": 500
}
]
}
]
}
复制代码
这是基于 IP 的限流配置其中,前两个配置需要注意,当然官方文档也有具体解释,其他的就看英文字面意思即可需要注意的是 QuotaExceededResponse 这个配置是在触发限流规则时服务端返回的信息,根据文档的介绍自行配置即可,没啥可说的。这里,我先注释掉白名单配置里本机的 ip
"IpWhitelist": [ "192.168.0.0/24" ]
复制代码
然后访问接口,正常会是这样
由于我们在规则里设定的是每秒访问 2 次所以,当频繁访问的时候,就会这样
那如果我们想要不限制本机 ip 或者指定 IP 只需要把我们的 ip 地址写入到上述的白名单里
"IpWhitelist": [ "127.0.0.1", "::1", "192.168.0.0/24" ]
复制代码
或者,调整访问频率,比如每秒限制访问 5 次等,或者在 IpRateLimitPolicies 在单独配置个别特殊 ip 的访问规则。这里需要注意的是,如果配置了 IpRateLimitPolicies,需要在系统入口 Program.cs 中加载配置好的种子文件,这有点类似于 efcore 的配置,下面代码第 4-11 行就是配置 IpRateLimitPolicies 的代码。
public static async Task Main(string[] args)
{
IHost webHost = CreateHostBuilder(args).Build();
using (var scope = webHost.Services.CreateScope())
{
// get the IpPolicyStore instance
var ipPolicyStore = scope.ServiceProvider.GetRequiredService<IIpPolicyStore>();
// seed IP data from appsettings
await ipPolicyStore.SeedAsync();
}
await webHost.RunAsync();
}
复制代码
4.基于客户端的限流配置
这个部分也直接看文档的具体解释👉:https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/ClientRateLimitMiddleware我这里也结合官方案例,再贴几个测试的效果
//客户端ID限流
"ClientRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"EndpointWhitelist": [ "get:/api/license", "*:/api/status" ],
"ClientWhitelist": [ "dev-id-1", "dev-id-2","666" ],
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1s",
"Limit": 1
},
{
"Endpoint": "*",
"Period": "15m",
"Limit": 100
},
{
"Endpoint": "*",
"Period": "12h",
"Limit": 1000
},
{
"Endpoint": "*",
"Period": "7d",
"Limit": 10000
}
],
"QuotaExceededResponse": {
"Content": "{{\"code\":429, \"msg\": \"您的访问过于频繁,已经触发限流规则,每{1}允许访问{0}次,请{2}秒后在试\", \"data\": \"Quota exceeded. Maximum allowed: {0} per {1}. Please try again in {2} second(s).\" }}",
"ContentType": "application/json",
//"StatusCode": 429
"StatusCode": 200
}
},
"ClientRateLimitPolicies": {
"ClientRules": [
{
"ClientId": "dev-id-1",
"Rules": [
{
"Endpoint": "get:/cert/getCustomTitles",
"Period": "1s",
"Limit": 5
},
{
"Endpoint": "get:/cert/*",
"Period": "1m",
"Limit": 2
},
{
"Endpoint": "put:/api/clients",
"Period": "5m",
"Limit": 2
}
]
},
{
"ClientId": "cl-key-2",
"Rules": [
{
"Endpoint": "*",
"Period": "1s",
"Limit": 10
},
{
"Endpoint": "get:/api/clients",
"Period": "1m",
"Limit": 0
},
{
"Endpoint": "post:/api/clients",
"Period": "5m",
"Limit": 50
}
]
},
{
"ClientId": "cl-key-3",
"Rules": [
{
"Endpoint": "post:/api/clients",
"Period": "1s",
"Limit": 3
}
]
}
]
}
复制代码
这里的配置和基于 ip 的配置过程是一样需要注意的是我们配置的 ClientIdHeader 里的属性,需要自己在发送请求时实现。我这里的属性定义名字就是官方默认的“X-ClientId”,那在请求的时候,要在 header 里加上这个配置,我这里的请求以来的 axios
axios.get("/Examination/GetExaminations", {
params: {
"associationId": associationId,
"examId": examId,
"groupCode":groupCode
}, headers: {
"X-ClientId": getCookie("userid")
}
}).then(v=>{
//具体逻辑省略
})
复制代码
这样在发送请求的时候,请求头里就会带上一个 X-ClientId
同时,响应头里也会返回一些限流信息
这里它的限流策略是从内而外的,比如最内层设置了每秒限制访问 5 次,第二层设置了每小时访问 200 次,最外层设置了每 7 天访问 10000 次,那响应头里就会命中符合条件的最外层策略。同样的,这里如果我们把 X-ClientId 的值,放到白名单里,也会跳过限制,反之则会触发限制策略
如果请求头里去掉 X-ClientId,同样会触发限流
总结
除了本文介绍的内容,官方文档里还有一些其他的内容,比如Behavior,Update rate limits at runtime等。
总的来说,结合 AspNetCoreRateLimit 中间件,可以很方便的给我们的项目增加限流机制,同时其丰富灵活的配置策略也为我们提供了广泛的适用场景。
如果有这方面需要的小伙伴,可以试一下!
沉痛悼念
最后,在笔者准备写这篇博客的时候,即 2023 年 5 月 15 日下午,突然收到推送消息,资深技术博主,架构师,MegaEase CEO 陈皓(网名左耳朵耗子)于上周六(5.13)晚,突发心梗去世。推送原文地址:https://mp.weixin.qq.com/s/ORBGjDNBet3mgZC7AzN2Cg。作为一名开发者,我多次翻阅陈皓老师的博客(https://coolshell.cn/),也看过很多他的公开课,获益良多,听闻噩耗,尤感痛心,愿一路走好。同时希望所有人,珍惜健康,注意饮食,适度锻炼,定期体检。
评论