在C#和.NET Core中为公共端点实现限流以缓解暴力攻击
在无请求发起者标识的C#/.NET Core公共端点实现限流
场景说明
这里的“无法识别请求发起者”通常分为两种常见情况:
- 无用户登录态,但可获取请求的客户端IP(大多数公共端点的常规场景)
- 所有请求来自统一代理/网关,无法通过IP区分具体发起者(极端场景)
以下是对应场景的具体实现方案:
场景1:基于客户端IP的限流(推荐)
虽然没有用户身份标识,但客户端IP是最可靠的请求来源区分依据,也是公共端点限流的标准做法。可以通过第三方库快速实现,也能自定义中间件完成。
方式1:使用AspNetCoreRateLimit库
这是.NET生态中成熟的限流库,支持IP、路径等多维度规则:
- 安装NuGet包:
Install-Package AspNetCoreRateLimit
- 在
Program.cs中配置服务:
builder.Services.AddMemoryCache(); // 配置IP限流规则 builder.Services.Configure<IpRateLimitOptions>(options => { options.GeneralRules = new List<RateLimitRule> { new RateLimitRule { Endpoint = "*:/api/xx", // 目标端点路径,*匹配任意HTTP方法 Period = "1m", // 时间窗口(1分钟) Limit = 3 // 窗口内允许的请求次数 } }; }); // 注册限流所需的存储和配置服务 builder.Services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>(); builder.Services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>(); builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>(); builder.Services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
- 注册中间件(注意要放在路由中间件之前):
app.UseIpRateLimiting(); app.UseRouting(); // 其他中间件...
方式2:自定义IP限流中间件
如果不想依赖第三方库,可以手写轻量中间件:
public class IpRateLimitMiddleware { private readonly RequestDelegate _next; // 用并发字典存储IP的请求计数和最后请求时间 private static readonly ConcurrentDictionary<string, (int Count, DateTime LastRequestTime)> _requestCache = new(); // 限流参数可根据业务调整 private const int MaxRequests = 3; private const int TimeWindowMinutes = 1; public IpRateLimitMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var targetPath = "/api/xx"; if (!context.Request.Path.Equals(targetPath, StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } var clientIp = context.Connection.RemoteIpAddress?.ToString(); if (string.IsNullOrEmpty(clientIp)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("无法识别请求来源"); return; } var now = DateTime.UtcNow; if (_requestCache.TryGetValue(clientIp, out var entry)) { // 检查是否在时间窗口内 if (now - entry.LastRequestTime <= TimeSpan.FromMinutes(TimeWindowMinutes)) { if (entry.Count >= MaxRequests) { context.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.Response.WriteAsync("请求过于频繁,请稍后再试"); return; } // 更新计数和时间 _requestCache[clientIp] = (entry.Count + 1, now); } else { // 时间窗口过期,重置计数 _requestCache[clientIp] = (1, now); } } else { // 首次请求,初始化记录 _requestCache.TryAdd(clientIp, (1, now)); } await _next(context); } }
然后在Program.cs中注册中间件:
app.UseMiddleware<IpRateLimitMiddleware>(); app.UseRouting();
场景2:全局维度限流(极端场景)
如果完全无法区分任何请求来源(比如所有请求都经过同一代理IP),只能对目标端点设置全局请求次数限制:
自定义全局限流中间件
public class GlobalRateLimitMiddleware { private readonly RequestDelegate _next; private static int _globalRequestCount = 0; private static DateTime _windowStartTime = DateTime.UtcNow; private const int MaxGlobalRequests = 3; private const int TimeWindowMinutes = 1; // 锁对象保证多线程下计数安全 private static readonly object _lockObj = new(); public GlobalRateLimitMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var targetPath = "/api/xx"; if (!context.Request.Path.Equals(targetPath, StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } lock (_lockObj) { var now = DateTime.UtcNow; // 时间窗口过期则重置计数 if (now - _windowStartTime > TimeSpan.FromMinutes(TimeWindowMinutes)) { _globalRequestCount = 0; _windowStartTime = now; } if (_globalRequestCount >= MaxGlobalRequests) { context.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.Response.WriteAsync("当前端点请求过于频繁,请稍后再试"); return; } _globalRequestCount++; } await _next(context); } }
注册方式与自定义IP限流中间件一致。
注意事项
- 内存存储的限流数据会在应用重启后丢失,若需要持久化,可改用Redis等分布式存储(
AspNetCoreRateLimit原生支持Redis) - 时间窗口和请求次数需根据业务场景调整,避免误拦截合法请求
- 返回
429 Too Many Requests状态符符合HTTP规范,可额外添加Retry-After响应头告知客户端重试时间 - 针对IP限流,需考虑同一局域网用户共用IP的情况,可根据业务灵活调整规则
内容的提问来源于stack exchange,提问作者Claudio Riquelme
相关产品推荐
相关产品推荐

