使用ASP.NET Core中的Rate Limiting限流保护API接口安全插图

使用ASP.NET Core中的Rate Limiting限流:为你的API接口穿上“防弹衣”

大家好,作为一名经常和API打交道的开发者,我猜你一定遇到过这样的场景:某个深夜,监控告警突然狂响,服务器CPU和内存直线飙升,一查日志,发现某个接口正被某个IP以每秒数百次的频率疯狂调用。这可能是恶意攻击,也可能是自家前端代码写了个死循环。无论原因如何,结果都是一样的——服务被拖垮,正常用户无法访问。这种时候,限流(Rate Limiting) 就是保护我们API安全与稳定的第一道,也是至关重要的一道防线。今天,我就结合自己的实战和踩坑经验,带大家深入了解一下ASP.NET Core 7及更高版本中内置的强大限流功能,手把手教你如何为接口穿上这件“防弹衣”。

一、为什么我们需要Rate Limiting?

在深入代码之前,我们得先统一思想。限流不仅仅是技术实现,更是一种架构理念。从我经历过的几次线上事故来看,它的核心价值在于:

  • 防止资源枯竭:避免单个用户或客户端耗尽服务器资源(如数据库连接、线程、带宽)。
  • 抵御暴力攻击:对登录、注册、短信验证码等接口进行频率限制,是防范撞库、短信轰炸等攻击的基础手段。
  • 保证服务公平性:确保所有用户都能公平地使用服务,避免少数“狂热”用户影响大多数正常用户。
  • 为扩容争取时间:当流量意外陡增时,限流可以作为“断路器”,为运维团队分析问题和弹性扩容赢得宝贵时间。

ASP.NET Core 7之前,我们可能需要依赖像 AspNetCoreRateLimit 这样的第三方库。而现在,微软官方终于推出了成熟、高性能的内置方案,集成和使用都变得更加优雅。

二、快速入门:给你的API加上全局“速度表”

让我们从一个最简单的全局限流开始。假设我们想限制整个应用每分钟最多处理100个请求。

首先,在 Program.cs 中添加限流服务并配置一个固定窗口限流器:

// 添加限流服务
builder.Services.AddRateLimiter(options =>
{
    // 定义一个名为“Fixed”的全局策略
    options.GlobalLimiter = PartitionedRateLimiter.Create(
        partitioner: context => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: "Global", // 分区键,这里所有请求视为一个分区
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100, // 时间窗口内允许的请求数
                Window = TimeSpan.FromMinutes(1), // 时间窗口长度
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 2 // 允许排队等待的请求数
            }
        ));
    // 配置当被限流时的响应
    options.OnRejected = (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        return new ValueTask();
    };
});

// ... 其他服务配置

var app = builder.Build();

// 使用限流中间件(必须在UseRouting之后,UseEndpoints之前)
app.UseRateLimiter();

app.MapControllers();
app.Run();

踩坑提示UseRateLimiter() 的调用位置非常关键!它必须在 UseRouting() 之后,这样才能获取到终结点(Endpoint)信息以便应用更细粒度的策略;同时必须在 UseEndpoints()MapControllers() 之前。顺序错了,策略可能不会生效。

现在,启动你的应用并快速刷新浏览器,当一分钟内的请求超过100次后,你就会收到一个429状态码(Too Many Requests)。一个基础的全局限流就完成了。

三、进阶策略:四种“限流算法”如何选择?

ASP.NET Core内置了四种经典的限流算法,应对不同场景:

  1. 固定窗口(Fixed Window):就像我们上面用的。将时间切成连续的固定窗口(如每分钟),每个窗口内计数。简单高效,但可能在窗口交界处承受两倍流量冲击。
  2. 滑动窗口(Sliding Window):更平滑,将窗口划分为多个子段,每次移动一个子段。能更好地应对窗口边界问题,但计算稍复杂。
  3. 令牌桶(Token Bucket):系统以恒定速率向桶中添加令牌,请求需要拿到令牌才能通过。允许一定程度的突发流量(取决于桶容量),非常常用。
  4. 并发(Concurrency):严格限制同时处理的请求数量,超过的直接拒绝或排队。适用于保护资源池(如数据库连接)。

下面,我们以最常用的令牌桶算法为例,配置一个针对用户登录接口的限流:

builder.Services.AddRateLimiter(options =>
{
    // 添加一个名为“TokenBucketPolicy”的策略
    options.AddTokenBucketLimiter(policyName: "TokenBucketPolicy", options =>
    {
        options.TokenLimit = 10; // 桶容量
        options.TokensPerPeriod = 5; // 每个周期添加的令牌数
        options.ReplenishmentPeriod = TimeSpan.FromSeconds(10); // 补充周期
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 2;
    });

    options.OnRejected = (context, token) => { /* ...同上... */ };
});

然后,在控制器或Minimal API中应用这个策略:

// 在Controller的Action上使用
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    [HttpPost("login")]
    [EnableRateLimiting("TokenBucketPolicy")] // 应用策略
    public IActionResult Login([FromBody] LoginModel model)
    {
        // 登录逻辑...
        return Ok();
    }
}

// 或者在Minimal API中
app.MapPost("/api/auth/login", () => { /* ... */ })
   .RequireRateLimiting("TokenBucketPolicy"); // 应用策略

这样,/api/auth/login 接口就受到了令牌桶策略的保护。它允许短时间内的少量突发(最多10次请求),但长期来看平均速率被限制在每10秒5次(即0.5次/秒)。

四、实战技巧:基于客户端IP或用户ID的精细化限流

全局限流太粗犷,我们通常需要更精细的控制,比如按IP限流以防止单个恶意IP攻击,或者按用户ID限流以保证用户间的公平性。

这就要用到 PartitionedRateLimiter 中的 partitionKey。以下是一个按IP限流,防止爬虫刷数据的例子:

options.AddFixedWindowLimiter("PerIPPolicy", opt =>
{
    opt.PermitLimit = 30;
    opt.Window = TimeSpan.FromMinutes(1);
}).AddPolicy("PerIPPolicy", context =>
{
    // 从HttpContext中获取客户端IP作为分区键
    // 注意:直接取 RemoteIpAddress 需要考虑代理和负载均衡(如X-Forwarded-For头),生产环境需更严谨处理
    var ip = context.Connection.RemoteIpAddress?.ToString();
    if (string.IsNullOrEmpty(ip))
    {
        ip = "unknown";
    }
    return RateLimitPartition.GetFixedWindowLimiter(ip, key => new FixedWindowRateLimiterOptions
    {
        PermitLimit = 30,
        Window = TimeSpan.FromMinutes(1)
    });
});

重要踩坑点:获取真实客户端IP在生产环境中是个复杂问题。如果应用前方有反向代理(Nginx)、负载均衡器(ELB)或CDN,RemoteIpAddress 拿到的是最后一个代理的IP。你必须配置中间件(ForwardedHeadersMiddleware)来读取 X-Forwarded-For 这样的头部,并从中提取最左边的可信IP。这一步没做好,按IP限流就会完全失效!

对于按用户限流,我们可以在用户认证后,从 HttpContext.User 中提取用户ID作为分区键,这样策略就能精确作用于每个登录用户。

五、自定义响应与集成Swagger

默认的429响应可能对前端不够友好。我们可以自定义响应体,返回JSON格式的错误信息:

options.OnRejected = async (context, token) =>
{
    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    context.HttpContext.Response.ContentType = "application/json";
    var message = JsonSerializer.Serialize(new
    {
        Code = 429,
        Message = "请求过于频繁,请稍后再试。",
        // 甚至可以返回重试等待时间(Retry-After)
        Details = $"触发策略: {context.Lease.MetadataName}"
    });
    await context.HttpContext.Response.WriteAsync(message, token);
};

另外,如果你使用了Swagger(OpenAPI),为了让前端开发者清楚接口的限流策略,最好在文档中注明。虽然内置限流目前不会自动生成OpenAPI注释,但我们可以手动添加描述:

[HttpPost("login")]
[EnableRateLimiting("TokenBucketPolicy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status429TooManyRequests, Type = typeof(ErrorResponse))]
[SwaggerOperation(Summary = "用户登录", Description = "此接口受令牌桶限流保护(10容量,每10秒补充5个令牌)。")]
public IActionResult Login([FromBody] LoginModel model)
{
    // ...
}

六、总结与最佳实践建议

经过上面的步骤,你应该已经掌握了在ASP.NET Core中使用内置限流器的基础和进阶技巧。最后,分享几点从实战中总结的建议:

  1. 分层设计:不要只依赖应用层限流。结合网关层(如Kong, Azure API Management)的全局限流和WAF的IP黑名单,构建纵深防御体系。
  2. 策略适度:限流值不是越小越好。需要结合业务压力测试、历史流量数据和用户体验来设定。对于关键业务接口,可以适当放宽或采用令牌桶允许合理突发。
  3. 监控与告警:一定要监控429状态码的数量和来源。突然飙升的429可能意味着攻击,也可能意味着你的业务量真的增长了,需要扩容。我在源码库的其他文章里会详细介绍监控搭建。
  4. 测试!测试!测试!:在压测(Load Test)中验证你的限流策略是否按预期工作。模拟高并发请求,观察响应码和系统资源。

ASP.NET Core内置的Rate Limiting模块功能强大且性能优异,它不再是“可选项”,而是现代API开发中的“必选项”。希望这篇教程能帮助你顺利地为自己的接口项目装上这道安全闸门,让服务运行得更稳、更安心。如果在实践中遇到任何问题,欢迎在评论区交流讨论!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。