写点什么

对不起,您的访问次数已用尽!

作者:为自己带盐
  • 2023-05-15
    河北
  • 本文字数:7367 字

    阅读完需:约 24 分钟

对不起,您的访问次数已用尽!

前言

我们在日常使用一些公有接口或者访问一些资源类网站的时候,经常会遇到如本文标题所示的一些提示,当前其目的也很明确,就是为了保证能有更多人更广泛的访问到资源,而对每个人的访问做出了一些限制。如近期大火的 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.引入中间件

  • 必须: AspNetCoreRateLimit

  • 非必须: AspNetCoreRateLimit.Redis


这里,如果只是实现限流功能,引入第一个包就可以了,但由于当前的开发的系统,大部分都是支持分布式部署,所以在配置限流中间件时,要考虑分布式的情况,这时就需要用到第二个包。

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 的分布式存储方式来存储计数规则,也可以采用内存形式

  • Redis 的部署采用了哨兵模式(sentinel),也可以采用单节点模式

  • 配置了两种限流方式,实际也可以任选其一


具体的注释,在代码里了。而涉及到 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,同样会触发限流


总结

除了本文介绍的内容,官方文档里还有一些其他的内容,比如BehaviorUpdate rate limits at runtime等。

总的来说,结合 AspNetCoreRateLimit 中间件,可以很方便的给我们的项目增加限流机制,同时其丰富灵活的配置策略也为我们提供了广泛的适用场景。

如果有这方面需要的小伙伴,可以试一下!

沉痛悼念

最后,在笔者准备写这篇博客的时候,即 2023 年 5 月 15 日下午,突然收到推送消息,资深技术博主,架构师,MegaEase CEO 陈皓(网名左耳朵耗子)于上周六(5.13)晚,突发心梗去世。推送原文地址:https://mp.weixin.qq.com/s/ORBGjDNBet3mgZC7AzN2Cg。作为一名开发者,我多次翻阅陈皓老师的博客(https://coolshell.cn/),也看过很多他的公开课,获益良多,听闻噩耗,尤感痛心,愿一路走好。同时希望所有人,珍惜健康,注意饮食,适度锻炼,定期体检。


发布于: 刚刚阅读数: 4
用户头像

学着码代码,学着码人生。 2019-04-11 加入

努力狂奔的小码农

评论

发布
暂无评论
对不起,您的访问次数已用尽!_.net core_为自己带盐_InfoQ写作社区