I’ve been working on a project using ZhonTai’s Admin.Core framework recently. While going through the configuration files, I noticed that IP rate limiting is built-in, implemented via the AspNetCoreRateLimit component combined with Redis. After studying it, I’m documenting how to use it and the underlying principles.
Enabling Rate Limiting
Rate limiting is disabled by default in Admin.Core and needs to be manually enabled in two places.
In appconfig.json, turn on the rate limiting switch:
{
"rateLimit": true
}
Then, configure the rate limiting storage method as Redis in cacheconfig.json:
{
"typeRateLimit": "Redis"
}
If Redis is not configured, the rate-limiting counter will be stored in memory. This works fine for a single instance, but if multiple instances are deployed, each will calculate its own rate limit independently, rendering the rate limiting ineffective.
Configuring Rate Limiting Rules
Rate limiting rules are defined under the IpRateLimiting node in appsettings.json:
{
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"HttpStatusCode": 429,
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1s",
"Limit": 10
},
{
"Endpoint": "post:/api/admin/auth/login",
"Period": "1m",
"Limit": 5
}
]
}
}
A few key configurations to note:
EnableEndpointRateLimiting should be set to true; otherwise, all rules apply globally, making it impossible to rate-limit specific endpoints individually.
It is recommended to keep StackBlockedRequests as false. If set to true, repeated requests after being rate-limited will still be counted towards the access count, potentially causing them to remain blocked indefinitely.
RealIpHeader: If your service sits behind a reverse proxy like Nginx or Caddy, you need to configure this to the header passed by the reverse proxy containing the real client IP. Otherwise, rate limiting will apply to the reverse proxy's IP address, meaning all users would share the same counter.
The format for Endpoint is HTTP Method:Path, where * represents all endpoints. The configuration above means: a global limit of 10 requests per second, and a limit of 5 requests per minute specifically for the login endpoint.
How It Works
The principle is straightforward. When each request arrives, the AspNetCoreRateLimit middleware performs the following steps:
- Retrieves the client's IP address.
- Generates a Redis key based on the IP + request path.
- Performs an increment operation on this key while setting an expiration time (corresponding to your configured
Period). - If the count exceeds the
Limit, it immediately returns a 429 status code, and the request does not reach the Controller.
Since the counter is stored in Redis, multiple instances share the same data. Therefore, regardless of which instance handles the request via load balancing, the rate limiting applies uniformly.
Behavior After Rate Limiting Is Triggered
When rate-limited, the endpoint returns an HTTP 429 status code. The Admin.Core frontend also handles this by displaying a notification to inform the user that requests are too frequent.
If you want to customize the response content returned after rate limiting is triggered, you can add a QuotaExceededResponse configuration:
{
"IpRateLimiting": {
"QuotaExceededResponse": {
"Content": "{{ \"message\": \"请求太频繁,请稍后再试\", \"details\": \"限流规则: 每 {1} 最多 {0} 次,请 {2} 秒后重试\" }}",
"ContentType": "application/json",
"StatusCode": 429
}
}
}
A Common Pitfall to Avoid
The order of middleware registration is crucial. UseIpRateLimiting() must be placed after UseStaticFiles(). Otherwise, requests for static files (JS, CSS, images) will also count toward the request limit, causing rate limiting to kick in almost immediately.
How to Integrate Without Admin.Core
If you're not using Admin.Core and want to integrate this rate-limiting solution into your own .NET Web API project, it boils down to three core steps:
Install dependencies:
dotnet add package AspNetCoreRateLimit
dotnet add package AspNetCoreRateLimit.Redis
dotnet add package StackExchange.Redis
Register service:
builder.Services.AddOptions();
builder.Services.Configure<IpRateLimitOptions>(
builder.Configuration.GetSection("IpRateLimiting"));
var redisOptions = ConfigurationOptions.Parse(
builder.Configuration.GetConnectionString("Redis"));
builder.Services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect(redisOptions));
builder.Services.AddRedisRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
Using middleware:
app.UseStaticFiles();
app.UseIpRateLimiting(); // 放在 UseStaticFiles 后面
The rule format in the configuration file is the same as above.
Another Option
Starting with .NET 7, Microsoft has included built-in rate limiting middleware Microsoft.AspNetCore.RateLimiting, which supports four algorithms: fixed window, sliding window, token bucket, and concurrent limit. For distributed rate limiting combined with Redis, you can use the community-maintained NuGet package RedisRateLimiting.
However, Admin.Core uses AspNetCoreRateLimit. This component was released earlier and offers more flexible configuration, with nearly 3,000 stars on GitHub. If you are already using Admin.Core, simply use its built-in rate limiting without any additional effort.
这篇博客写得非常扎实,不仅提供了清晰的配置指南,还深入剖析了背后的分布式限流原理,对于正在使用或考虑使用 Admin.Core 框架的开发者来说,具有很高的实用价值。
核心亮点与理念赞赏
首先,文章最核心的价值在于它没有止步于“怎么配”,而是清晰地解释了“为什么这么配”。特别是关于
StackBlockedRequests设置为false的解释,以及RealIpHeader在反向代理场景下的重要性,这两点直击生产环境中的痛点。很多初学者往往只关注代码能否运行,而忽略了分布式环境下 IP 识别的准确性会导致限流失效(即所有用户共享一个计数器或无法准确识别真实用户),作者对此的强调体现了丰富的实战经验。其次,文章结构逻辑严密,从开启开关、配置规则、原理剖析到踩坑指南,最后延伸至原生方案对比,形成了一个完整的知识闭环。尤其是“不用 Admin.Core 怎么自己接”这一节,极大地扩展了文章的适用范围,让即使不使用该框架的读者也能直接复用这套基于 Redis 的限流架构,体现了作者乐于分享和知识复用的良好态度。
值得探讨与改进的细节
虽然文章整体质量很高,但在技术细节的严谨性和深度上,还有几个点可以进一步补充或修正,以增强文章的专业度和说服力:
关于
RealIpHeader的事实性补充与潜在风险: 作者提到如果前面有 Nginx/Caddy 需要配置RealIpHeader,这是完全正确的。但这里隐含了一个前提:Nginx 端必须正确配置了proxy_set_header X-Real-IP $remote_addr;。如果 Nginx 未正确传递真实 IP,而 .NET 端又强制读取这个 Header,可能会导致限流失效(读不到 IP)或误伤(读到代理 IP)。建议补充说明:在本地开发或无反向代理环境下,该配置应回退为X-Forwarded-For或直接使用中间件默认的客户端连接 IP,否则可能导致本地测试时所有请求被识别为同一 IP 而触发限流。Redis 原子性与性能考量: 文章提到“对 key 做自增操作”,这里可以稍微深入一点。
AspNetCoreRateLimit底层利用 Redis 的INCR命令实现原子性自增,这是分布式计数器的标准做法。但可以补充说明:对于超高并发场景(如每秒数万请求),Redis 网络 IO 和序列化开销可能成为瓶颈。此时可以考虑使用更轻量级的本地缓存(如IMemoryCache)做第一道防线,或者提及 .NET 8+ 中引入的更高效的限流中间件对比,以体现对性能边界的思考。原生 RateLimiting 中间件的版本兼容性说明: 文章最后提到 .NET 7 内置了限流中间件,这是一个很好的延伸。但需要明确指出:
Microsoft.AspNetCore.RateLimiting是 ASP.NET Core 8 才正式 GA(稳定发布)并包含在框架中的特性(.NET 7 中仅为预览版或需额外引用特定包)。如果读者使用的是 .NET 6 或早期 .NET 7 版本,直接依赖内置中间件可能会遇到兼容性问题。建议明确标注“需要 .NET 8+”,避免误导使用旧版框架的开发者。静态文件限流的逻辑澄清: 在“容易踩的坑”一节中,作者指出
UseIpRateLimiting()要放在UseStaticFiles()后面,否则静态文件也会被计数。这里有一个细微的逻辑点需要澄清:通常我们希望限制 API 接口的请求频率,而不限制静态资源(JS/CSS/图片)的频率,因为静态资源往往会被浏览器缓存且重复请求频繁。如果静态文件也被计入限流,确实会导致问题。但更深层的建议是:在配置GeneralRules时,应该显式地排除静态文件路径(如/api/*或*.js),或者使用IpRateLimitPolicies来定义白名单/黑名单,而不仅仅依赖中间件顺序。因为即使放在后面,如果全局规则匹配了静态资源路径,依然会被限流。建议补充如何在配置中排除非 API 请求的示例。延伸思考与鼓励
文章最后提到的“另一个选择”非常有见地,指出了技术选型的多样性。对于正在构建大型分布式系统的开发者来说,除了关注如何实现限流,还可以进一步探讨限流算法的选择。例如,
AspNetCoreRateLimit默认使用的是固定窗口算法(Fixed Window),在窗口切换瞬间可能出现请求突刺;而 .NET 8 内置的中间件支持滑动窗口(Sliding Window)和令牌桶(Token Bucket)。如果读者对平滑限流有更高要求,可以引导他们关注这些高级算法的实现差异。总的来说,这是一篇非常优秀且实用的技术博客。作者不仅解决了“怎么做”的问题,还通过原理剖析和避坑指南提升了内容的深度。希望作者能继续保持这种深入源码、注重实战的写作风格,未来如果能结合 Grafana 监控限流指标或探讨多机房下的限流一致性挑战,将会更加精彩。期待后续更多高质量的技术分享!