ASP.NET Core 的速率限制中间件可以限制请求频率或同时处理的请求数量。先注册限流服务,再把策略应用到指定端点;也可以配置全局限制器,为整个应用提供统一规则。
限流可用于控制资源消耗、改善服务稳定性、保护昂贵的处理流程,以及按用户或套餐分配请求额度。策略需要依据应用负载、端点成本和用户体验来设定,不能照搬一个数值就假定达到性能目标。中间件处理的是进入应用的请求,不能替代网络边缘防护,也不能独立保证抵御 DDoS 攻击。
原文列举的用途还包括防止公开 API 滥用、避免单个客户独占资源、缓解服务器与下游压力、减轻 DoS 风险、维持响应能力,以及控制按调用量计费的成本。DDoS 则可能由大量系统同时发起,仅靠应用限流难以处理。专门防护通常结合流量分析、大规模流量承载、自动缓解、全球网络和持续更新;可按既有托管方案查阅 Azure Web Application Firewall、AWS Shield、Google Cloud Armor、Cloudflare DDoS 防护等官方资料。本文只说明原文所列方案,不替任何服务作性能保证,也不执行订购。
以下采用 ASP.NET Core 10.0 官方文档。代码包括完整程序和配置片段;片段必须放进对应项目上下文,不是每一段都能独立编译。示例项目中的 MyRateLimitOptions、ErrorViewModel、视图与命名空间需要从官方样例取得或按项目实现。本文仅做静态核对,没有运行服务器或压力测试。
注册服务与中间件
使用 AddRateLimiter 注册服务。以下全局限制器按用户身份名称分区;没有名称时使用 Host,每个分区每分钟最多取得 10 个许可,不允许排队。
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
Host 是请求输入,不能当作已验证的用户身份。上例展示分区接口,实际部署应采用经过验证、数量受控的分区键,防止不断产生新键导致大量缓存限制器。
命名策略则为后续端点提供可引用的名称,例如注册 fixed:每 12 秒四个许可,最多两个请求排队,先到先处理。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 4;
opt.Window = TimeSpan.FromSeconds(12);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 2;
});
});
var app = builder.Build();
启用中间件时,使用端点策略必须让 UseRateLimiter 位于 UseRouting 之后,使中间件能读取选中的端点元数据。官方中文页面在这一处存在顺序表述错误;以下顺序按英文正文及代码核对。如果分区依赖认证用户,还必须在限流之前完成认证。
app.UseRouting();
app.UseRateLimiter();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
只有全局限制器而不使用端点策略时,限流可以放在路由之前。使用 Minimal API 的简化管线时还要考虑框架自动插入的路由行为;不要把一个显式 UseEndpoints 片段与完整 Minimal API 程序重复拼接。
将策略应用到端点或页面
Web API
在端点构建器上调用 RequireRateLimiting,引用已经注册的策略名称:
app.MapGet("/api/resource", () => "This endpoint is rate limited")
.RequireRateLimiting("fixed"); // Apply specific policy to an endpoint
MVC 控制器
对 MapControllers 的结果调用该方法,把策略应用到映射的控制器端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireRateLimiting("fixed");
});
服务器端 Blazor
可以对 Razor Components 映射应用策略:
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.RequireRateLimiting("policy");
也可以在 Razor 组件中使用属性声明:
@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]
<h1>Counter</h1>
这个属性可以加在可路由组件上,或通过 _Imports.razor 应用于一整个组件目录。override 必须是已注册策略;它替换该端点原有命名策略,全局限制器仍运行。仅在 MapRazorComponents 上没有调用 RequireRateLimiting 时使用这一属性方式;需要禁用时可以使用 DisableRateLimiting。
属性只影响相应 HTTP 端点的处理,不应据此推断它对已经建立的交互连接中的每个事件都重新限流。端点上的显式策略与组件或控制器属性有优先级关系,见后面的属性示例。
选择限制器算法
固定窗口
固定窗口在每个固定时间段内发放指定数量的许可。窗口切换时额度重置。它简单直接,但两个相邻窗口边界附近可能短时间接受两批请求。
下面的完整 Minimal API 程序每 12 秒允许四次请求,排队上限为两次:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 4;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
.RequireRateLimiting("fixed");
app.Run();
策略参数应从配置绑定,便于按环境调整。以下使用官方样例的 MyRateLimitOptions;配置节和模型定义不在这段代码中,必须一并提供。详见配置指南。
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
.RequireRateLimiting(fixedPolicy);
app.Run();
滑动窗口
滑动窗口把窗口拆成多个小段。每过一个小段,窗口向前移动,落到窗口之外的旧段额度被回收,缓和固定边界产生的突发。
例如,30 秒窗口划分为三个 10 秒段,总额度 100。下表沿用官方教学数据;“回收”是在本次段切换时重新可用的许可,不是任务完成后返回的许可。
| 时间(秒) | 回收前可用 | 本次使用 | 回收 | 剩余 |
|---|---|---|---|---|
| 0 | 100 | 20 | 0 | 80 |
| 10 | 80 | 30 | 0 | 50 |
| 20 | 50 | 40 | 0 | 10 |
| 30 | 10 | 30 | 20 | 0 |
| 40 | 0 | 10 | 30 | 20 |
| 50 | 20 | 10 | 40 | 50 |
| 60 | 50 | 35 | 30 | 45 |
通过 SegmentsPerWindow 指定分段数量,其他配置类似固定窗口:
每个小段的时长为 Window / SegmentsPerWindow,每次前移一段会回收一个完整窗口之前的过期段中使用的许可。官方图的蓝色对角线表示当前段随时间前移,红线表示从 30 秒起回收过期段;上表保留了同一组数值。
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
.RequireRateLimiting(slidingPolicy);
app.Run();
令牌桶
令牌桶的容量由 TokenLimit 决定,每过 ReplenishmentPeriod 补充 TokensPerPeriod 个令牌,超过容量的部分不累积。它允许在积攒令牌后出现一定突发,同时控制持续补充速度。
例如桶容量 100,每 10 秒最多补充 20。实际补入不能使令牌数超过容量。官方示例中各时刻的使用与补充如下:
| 时间(秒) | 补充前可用 | 本次使用 | 实际补充 | 剩余 |
|---|---|---|---|---|
| 0 | 100 | 20 | 0 | 80 |
| 10 | 80 | 10 | 20 | 90 |
| 20 | 90 | 5 | 15 | 100 |
| 30 | 100 | 30 | 20 | 90 |
| 40 | 90 | 6 | 16 | 100 |
| 50 | 100 | 40 | 20 | 80 |
| 60 | 80 | 50 | 20 | 50 |
令牌桶不需要像滑动窗口那样记录每个时间段已用的许可。直接构造令牌桶时,AutoReplenishment=true 使用内部计时器;关闭后应用须调用 TryReplenish。下例通过框架辅助方法注册,辅助方法使用共享补充机制;其行为与直接构造实例的独立计时器区别见链式策略一节。
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
.RequireRateLimiting(tokenPolicy);
app.Run();
并发限制
并发限制器约束正在同时处理的请求数,不限制某段时间内累计请求数。请求结束后会释放并发许可;它没有时间窗口或定时补充的配置。
以下通过 500 毫秒延迟展示有持续时间的请求,但没有实测并发数或响应时间:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", async () =>
{
await Task.Delay(500);
return Results.Ok($"Concurrency Limiter {GetTicks()}");
}).RequireRateLimiting(concurrencyPolicy);
app.Run();
按客户端或端点分区
分区使不同请求群体拥有各自的限制器。可以实现按客户公平分配、按套餐设置不同额度、隔离昂贵端点,以及避免一个群体消耗全部额度。
每个不同的分区键会创建并缓存自己的限制器。用户控制且数量无界的键会消耗内存,形成拒绝服务风险。分区设计必须同时考虑身份可信度、键的数量和生命周期;“每个用户一个额度”不等于每次任意输入都创建新额度。
按 IP 地址
每个远端 IP 每分钟 50 次:
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 50,
Window = TimeSpan.FromMinutes(1)
}));
反向代理后的 RemoteIpAddress 可能是代理地址。只应按可信代理配置转发头解析,不能直接信任任意客户端提交的 X-Forwarded-For。共享出口的用户也可能共用一个 IP 额度。
按用户身份
以身份名称为键,匿名请求共享 anonymous 分区:
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
认证中间件应先于限流执行,且身份名称应符合项目的唯一性和稳定性要求。匿名用户共用额度会影响彼此,需结合登录和滥用防护策略设计。
按 API 密钥
官方例子用 X-API-Key 区分套餐。这里修正一个明确细节:缺失头的 ToString() 通常得到空字符串,?? "no-key" 并不能覆盖空值,因而改为显式判断空白。
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
string apiKey = httpContext.Request.Headers["X-API-Key"].ToString();
if (string.IsNullOrWhiteSpace(apiKey))
{
apiKey = "no-key";
}
// Different limits based on key tier
return apiKey switch
{
"premium-key" => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: apiKey,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1)
}),
_ => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: apiKey,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}),
};
});
premium-key 是教学占位字符串,不是密钥验证。不能因为请求头恰好包含该值就授权高级套餐;实际应先验证密钥,并只使用认证后的、数量受控的身份作为分区键。所有未知密钥若直接成为新键,攻击者可以绕开单分区额度并增加内存消耗。本文保留策略结构供理解接口,并不将它称为可直接上线的认证方案。
按端点路径
对 /api/public 前缀使用每 10 秒 30 次的规则,其他路径每分钟 100 次。组合键含 IP 和端点类别,使两类请求不共用同一分区:
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
string path = httpContext.Request.Path.ToString();
// Different limits for different paths
if (path.StartsWith("/api/public"))
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: $"{httpContext.Connection.RemoteIpAddress}-public",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 30,
Window = TimeSpan.FromSeconds(10)
});
}
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
});
});
创建链式限制器
可以把多个限制器串起来,让请求同时满足多条规则。以下全局链按 User-Agent 分区,要求同时满足“每两秒四次”和“每 30 秒二十次”,并在拒绝时返回 429 及可用的重试提示:
using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ =>
{
_.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
};
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromSeconds(2)
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(30)
});
}));
});
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));
app.Run();
User-Agent 可以由调用者任意改变,只适合展示分区用法,不能当作可靠的客户身份。这里的 RetryAfter 来自失败租约元数据;不同限制器并不都能估算等待时间,例如并发限制依赖现有请求何时结束。
链按声明顺序获取许可。若后一个限制器拒绝,请求仍可能消耗前一个时间型限制器的许可;可释放的并发许可则可以归还。将约束串联并不意味着所有计数都有事务回滚。
命名策略中的链
也可在 AddPolicy 中通过 RateLimitPartition.Get 为每个已认证身份建立链,同时限制令牌消耗和并发数。以下保留官方配置,补上原片段遗漏的中间件调用和 Run,并加入所需命名空间:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("combined", httpContext =>
{
// Partition on the authenticated identity name when available. Each distinct key creates and
// caches its own limiter, so partitioning on unbounded user-controlled
// input can exhaust memory (a DoS risk).
string partitionKey = httpContext.User.Identity?.Name ?? "anonymous";
return RateLimitPartition.Get(partitionKey, _ =>
RateLimiter.CreateChained(
new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 5,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 10,
AutoReplenishment = true
}),
new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 5,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2
})));
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/resource", () => "This endpoint uses multiple limiters")
.RequireRateLimiting("combined");
app.Run();
这里直接创建的令牌桶设置了 AutoReplenishment=true,每个实例有自己的计时器。AddTokenBucketLimiter 和 RateLimitPartition.GetTokenBucketLimiter 辅助方法则把内部实例的自动补充设为关闭,由共享计时器统一补充。自行管理的限制器还必须明确生命周期;RateLimiter.CreateChained 不会替你处置其中所有底层限制器。上例的分区缓存链,框架管理分区限制器的生命周期;在分区之外自行创建链时,要按实际所有权处置不再使用的底层限制器。
全局应用应优先使用 PartitionedRateLimiter.CreateChained,只想限制特定端点时采用命名策略里的 RateLimiter.CreateChained。二者返回类型和作用范围不同,不能仅凭方法名相同就互换。
拒绝请求时的响应
通过 RejectionStatusCode 设置状态码,例如 429:
builder.Services.AddRateLimiter(options =>
{
// Set a custom status code for rejections
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Rate limiter configuration...
});
也可以用 OnRejected 写响应和日志。下面是配置片段,logger 必须由项目上下文提供;原文里的固定 Retry-After: 60 仅为示例,实际提示应与策略和租约信息一致。
builder.Services.AddRateLimiter(options =>
{
// Rate limiter configuration...
options.OnRejected = async (context, cancellationToken) =>
{
// Custom rejection handling logic
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers["Retry-After"] = "60";
await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);
// Optional logging
logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
context.HttpContext.Connection.RemoteIpAddress);
};
});
写响应时使用传入的取消令牌,避免客户端取消后仍继续无谓工作。日志中包含 IP 等信息时,应按项目的隐私和保留政策处理。429 仅表示请求被限流,不自动代表调用方已经收到真实可用的重试时刻。
请求排队
额度不足时,可以立即拒绝,也可以允许最多 QueueLimit 个许可请求等待。QueueProcessingOrder.OldestFirst 先处理早到者;NewestFirst 优先较新的等待者,并可能在队列满时淘汰较旧的等待请求。队列不能无限替代容量规划。
以下固定窗口每 10 秒十个许可,最多五个请求等待:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", options =>
{
options.PermitLimit = 10; // Allow 10 requests
options.Window = TimeSpan.FromSeconds(10); // Per 10-second window
options.QueueLimit = 5; // Queue up to 5 additional requests
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Process oldest requests first
options.AutoReplenishment = true; // Default: automatically replenish permits
});
});
队列增加延迟和资源占用,需与客户端超时、取消和总体并发目标一起测试。只有获得许可后,等待请求才会继续进入后续中间件。
属性与端点策略的优先级
EnableRateLimiting 为控制器或操作指定命名策略,DisableRateLimiting 禁用该端点的限流。禁用属性会覆盖全局与命名规则;使用属性前必须确认端点确实允许不受这些规则约束。
Razor Pages 的属性应加在页面上,不能加在 OnGet、OnPost 等页面处理器方法上。这与控制器操作方法的属性位置不同。
第一种配置对 Razor Pages 显式应用 sliding,对默认控制器路由显式应用 fixed:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);
app.Run();
相应控制器:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
在这个配置里,端点映射上的 RequireRateLimiting("fixed") 优先于 Privacy 的 [EnableRateLimiting("sliding")],因此不能读到属性就认定该操作使用滑动窗口。NoLimit 的禁用属性则仍生效。
第二种配置不在端点映射上调用 RequireRateLimiting:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages();
app.MapDefaultControllerRoute(); // RequireRateLimiting not called
app.Run();
控制器属性保持相同:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
此时控制器类上的 fixed 作用于普通操作,Privacy 的操作级 sliding 覆盖类级规则,NoLimit 禁用限流。ErrorViewModel、Activity、视图和配置模型仍依赖完整官方样例项目,不能把两套 Program 代码同时放入一个程序。
指标与测试
速率限制器提供内置指标,可观察当前租约、等待和请求结果等行为。应把应用指标与实际响应、延迟、CPU、内存及下游服务负载一起分析。
测试限流端点可以先用 JMeter HTTP(S) Test Script Recorder 或 BlazeMeter 创建 JMeter 脚本,再导入 Azure Load Testing。先在隔离环境检查窗口内许可数、窗口边界、队列满时的响应、取消行为、不同用户分区和匿名分区;再用代表性负载确定参数。分区数量、端点处理成本(时间、数据访问、CPU、I/O)、队列和并发都会影响结果。
原文另提醒按客户端输入分区会增加 DoS 风险,按 IP 分区也需要考虑地址伪造相关的网络防护。可查阅 BCP 38 / RFC 2827:网络入口过滤,应用层分区不能替代网络边界的来源验证。
限流不足可能无法保护资源,过严则会拒绝正常用户。上线前要检查真实认证和代理配置、端点元数据、各策略名称、拒绝响应及恢复方式,并验证匿名请求不能通过修改任意请求头无限创建分区。本文没有执行上述测试,也没有给出已验证的性能或 DDoS 防护承诺。
进一步阅读
- 官方 ASP.NET Core 10.0 文档及样例入口。
- 原文所链的 Maarten Balliauw 限流介绍。
- System.Threading.RateLimiting API。
- ASP.NET Core 性能诊断。
- 官方滑动窗口示意图:分段移动后回收窗口外的许可;教学数值已在上表保留。
- 原文 DDoS 示例还列有 Akamai Kona Site Defender 历史入口;保留来源链接,不据此声称该产品名称或入口目前仍有效。
来源:ASP.NET Core 中的速率限制中间件,Arvin Kahbazi、Maarten Balliauw、Rick Anderson 及 Microsoft 文档贡献者。中文整理覆盖原文全部技术章节和 25 段示例,参照英文版修正中间件顺序表述,并明确修正 API 密钥空值处理与链式策略片段遗漏的中间件调用。正文按 CC BY 4.0 提供,许可依据为官方文档仓库 LICENSE;代码按官方 LICENSE-CODE的 MIT 条款,© Microsoft Corporation,完整声明随 LICENSE-code.txt 保存。本文注明上述修改,未执行代码或生成运行截图。
MIT 许可声明
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.











暂无评论内容