.Net Core結合AspNetCoreRateLimit實現限流


前言

  相信使用過WebApiThrottle的童鞋對AspNetCoreRateLimit應該不陌生,AspNetCoreRateLimit是一個ASP.NET Core速率限制的解決方案,旨在控制客戶端根據IP地址或客戶端ID向Web API或MVC應用發出的請求的速率。AspNetCoreRateLimit包含一個IpRateLimitMiddlewareClientRateLimitMiddleware,每個中間件可以根據不同的場景配置限制允許IP或客戶端,自定義這些限制策略,也可以將限制策略應用在每​​個API URL或具體的HTTP Method上。

實踐

   起初是因為新做的項目中,有天查詢日志發現,對外的幾個公共接口經常被“惡意”調用,考慮到接口安全性問題,增加限流策略。

  AspNetCoreRateLimit GayHub:https://github.com/stefanprodan/AspNetCoreRateLimit

根據IP進行限流

  通過nuget安裝AspNetCoreRateLimit,當前版本是3.0.5,因為實際項目中用的都是分布式緩存,在這里不用內存存儲,而是結合Redis進行使用,內存存儲直接參考官方的Wiki就可以了。

Install-Package AspNetCoreRateLimit 

Install-Package Microsoft.Extensions.Caching.Redis

  在Startup.ConfigureServices中將服務和其他依賴注入

public void ConfigureServices(IServiceCollection services)
        {
            #region MVC
            services.AddMvc(
              options =>
              {
                  options.UseCentralRoutePrefix(new RouteAttribute("api/"));
              }
              ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            #endregion

            services.AddDistributedRedisCache(options =>
            {
                options.Configuration = "127.0.0.1:6379,password=123456,connectTimeout=5000,syncTimeout=10000"; 
                options.InstanceName = "WebRatelimit";
            }); 
            //加載配置
            services.AddOptions();
            //從appsettings.json獲取相應配置
            services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
            
            //注入計數器和規則存儲
            services.AddSingleton<IIpPolicyStore, DistributedCacheIpPolicyStore>();
            services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();
            
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            //配置(計數器密鑰生成器)
            services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
        }

  在Startup.Configure啟用

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }
            //啟用限流,需在UseMvc前面
            app.UseIpRateLimiting();
            app.UseMvc();
        }

  為了不影響appsettings.json的美觀吧,可以新建一個RateLimitConfig.json,並Program中啟動加載中增加

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>().ConfigureAppConfiguration((host,config)=> 
                {
                    config.AddJsonFile($"RateLimitConfig.json", optional: true, reloadOnChange: true);
                });

  RateLimitConfig.json 配置如下:

{
  "IpRateLimiting": {
    //false則全局將應用限制,並且僅應用具有作為端點的規則* 。 true則限制將應用於每個端點,如{HTTP_Verb}{PATH}
    "EnableEndpointRateLimiting": true,
    //false則拒絕的API調用不會添加到調用次數計數器上
    "StackBlockedRequests": false,
   //注意這個配置,表示獲取用戶端的真實IP,我們的線上經過負載后是 X-Forwarded-For,而測試服務器沒有,所以是X-Real-IP
"RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 200, "QuotaExceededResponse": { "Content": "{{\"code\":429,\"msg\":\"訪問過於頻繁,請稍后重試\",\"data\":null}}", "ContentType": "application/json", "StatusCode": 200 }, "IpWhitelist": [ ], "EndpointWhitelist": [], "ClientWhitelist": [], "GeneralRules": [ { "Endpoint": "*:/api/values/test", "Period": "5s", "Limit": 3 } ] } }

  重要配置說明:

       QuotaExceededResponse 是自定義返回的內容,所以必須設置HttpStatusCodeStatusCode為200。

  GeneralRules是具體的策略,根據不同需求配置不同端點即可, Period的單位可以是s, m, h, dLimint是單位時間內的允許訪問的次數;

  IpWhitelist是IP白名單,本地調試或者UAT環境,可以加入相應的IP,略過策略的限制;

       EndpointWhitelist是端點白名單,如果全局配置了訪問策略,設置端點白名單相當於IP白名單一樣,略過策略的限制;

       其他配置項請參考Wiki:https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#setup

Fiddler開始測試

測試接口:http://127.0.0.1:5000/api/values/Test

        [HttpGet]
        public object test()
        {
            return "ok";
        }

調用結果:

 

 調用次數和剩余調用次數在Head可以看到,(吃我一個鏈接:https://www.cnblogs.com/EminemJK/p/12720691.html

 

 如果調用超過策略后,調用失敗,返回我們自定義的內容

 

 在Redis客戶端可以看到策略的一些情況,

 其他

  通常在項目中,Authorization授權是少不了了,加入限流后,在被限流的接口調用后,限流攔截器使得跨域策略失效,故重寫攔截器中間件,繼承IpRateLimitMiddleware 即可:

    public class IPLimitMiddleware : IpRateLimitMiddleware
    {
        public IPLimitMiddleware(RequestDelegate next, IOptions<IpRateLimitOptions> options, IRateLimitCounterStore counterStore, IIpPolicyStore policyStore, IRateLimitConfiguration config, ILogger<IpRateLimitMiddleware> logger)
            : base(next, options, counterStore, policyStore, config, logger)
        {
        }

        public override Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitRule rule, string retryAfter)
        {
            httpContext.Response.Headers.Append("Access-Control-Allow-Origin", "*");
            return base.ReturnQuotaExceededResponse(httpContext, rule, retryAfter);
        }
    }

  然后修改Startup.Configure

        //啟用限流,需在UseMvc前面
        //app.UseIpRateLimiting();
        app.UseMiddleware<IPLimitMiddleware>();
        app.UseMvc();        

  特別需要注意的坑是,在其他文章的教程中,他們會寫成:

        app.UseMiddleware<IPLimitMiddleware>().UseIpRateLimiting();//錯誤的演示 https://www.cnblogs.com/EminemJK/p/12720691.html

  這些寫你測試的時候會發現,

X-Rate-Limit-Remaining 遞減量會變成2,而正常的遞減量應該是1,舉栗子,配置如下:

        "Endpoint": "*:/api/values/test",
        "Period": "3s",
        "Limit": 1

表示3秒內可以訪問的次數是一次,當發生調用的時候會直接返回被限制的提示,而不能正常訪問接口,原因就是因為調用了兩次中間件。

最后

  AspNetCoreRateLimit還可以根據客戶端ID進行配置策略,具體可以看一下官方的Wiki吧。

 

#2020-06-09 遇到的BUG,並解決

2020-05-21 02:51:42,900 [10] ERROR System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at Microsoft.Extensions.Caching.Redis.RedisCache.RefreshAsync(String key, Nullable`1 absExpr, Nullable`1 sldExpr, CancellationToken token)
   at Microsoft.Extensions.Caching.Redis.RedisCache.GetAndRefreshAsync(String key, Boolean getData, CancellationToken token)
   at Microsoft.Extensions.Caching.Redis.RedisCache.GetAsync(String key, CancellationToken token)
   at Microsoft.Extensions.Caching.Distributed.DistributedCacheExtensions.GetStringAsync(IDistributedCache cache, String key, CancellationToken token)
   at AspNetCoreRateLimit.DistributedCacheRateLimitStore`1.GetAsync(String id, CancellationToken cancellationToken)
   at AspNetCoreRateLimit.IpRateLimitProcessor.GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken)
   at AspNetCoreRateLimit.RateLimitMiddleware`1.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at DigitalCertificateSystem.Handlers.ExceptionHandlerMiddleWare.Invoke(HttpContext context)

也不清楚是什么時候會出現這個問題,在QPS達到一定的時候,訪問網站非常非常慢,同一服務器的后台管理卻沒有問題,最終發現是這個組件的問題。於是,改造:

一、實現 IRateLimitStore<T>,引入自己的組件

/// <summary>
    /// 重寫使用我們的自己的redis
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RateLimitStore<T> : IRateLimitStore<T>
    {
        public Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default)
        {
            return RedisUtils.StringSetAsync(RedisKEY.UserBehaviorCache, id, JsonConvert.SerializeObject(entry), expirationTime);
        }

        public async Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default)
        {
            var stored = await RedisUtils.StringGetAsync(RedisKEY.UserBehaviorCache, id);

            return !string.IsNullOrEmpty(stored);
        }

        public async Task<T> GetAsync(string id, CancellationToken cancellationToken = default)
        {
            var stored = await RedisUtils.StringGetAsync(RedisKEY.UserBehaviorCache, id);

            if (!string.IsNullOrEmpty(stored))
            {
                return JsonConvert.DeserializeObject<T>(stored);
            }

            return default;
        }

        public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
        {
            return RedisUtils.KeyDeleteAsync(RedisKEY.UserBehaviorCache, id);
        }
    }
View Code

二、實現 IRateLimitCounterStore

    public class RedisCounterStore: RateLimitStore<RateLimitCounter?>, IRateLimitCounterStore
    {
        
    }
View Code

三、實現 IIpPolicyStore

    public class RedisIpPolicyStore : RateLimitStore<IpRateLimitPolicies>, IIpPolicyStore
    {
        private readonly IpRateLimitOptions _options;
        private readonly IpRateLimitPolicies _policies;
        public RedisIpPolicyStore(
            IOptions<IpRateLimitOptions> options = null,
            IOptions<IpRateLimitPolicies> policies = null)
        {
            _options = options?.Value;
            _policies = policies?.Value;
        }

         
        public async Task SeedAsync()
        {
            // on startup, save the IP rules defined in appsettings
            if (_options != null && _policies != null)
            {
                await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
            }
        }
    }
View Code

四、修改 Startup

                services.AddOptions();
                services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
                services.AddSingleton<IIpPolicyStore, RedisIpPolicyStore>();
                services.AddSingleton<IRateLimitCounterStore, RedisCounterStore>();
                services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
View Code

搞定


 2020-07-09

Redis使用的是 StackExchange.Redis,幫助類封裝在  https://github.com/EminemJK/Banana

直接nuget搜索 Banana.Utility

PM>Install-Package Banana.Utility

本文已獨家授權給xLong設計(ID:xlongsheji)公眾號發布


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM