using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Dpz.Core.Infrastructure.RateLimiting;

/// <summary>
/// 窗口期限流中间件
/// 检查 IP 是否处于封禁期,封禁期内延迟响应并返回 429
/// </summary>
public class RateLimitMiddleware(
    RequestDelegate next,
    IConfiguration configuration,
    IWebHostEnvironment webHostEnvironment,
    IIpRateLimitService rateLimitService,
    ILogger<RateLimitMiddleware> logger
)
{
    public async Task InvokeAsync(HttpContext context)
    {
        // 已登录用户和开发环境不参与限流
        if (
            context.User.Identity?.IsAuthenticated == true
            || webHostEnvironment.IsDevelopment()
            || IsIgnoredPath(context)
        )
        {
            await next(context);
            return;
        }

        var clientIp = context.Request.GetIpAddress();
        if (string.IsNullOrEmpty(clientIp))
        {
            await next(context);
            return;
        }

        var result = await rateLimitService.CheckLimitStatusAsync(clientIp, context.RequestAborted);
        if (result.IsAllowed)
        {
            await next(context);
            return;
        }

        await RespondBlockedAsync(context, result);
    }

    /// <summary>
    /// 忽略列表内的请求不参与限流,与机器人拦截中间件保持一致
    /// </summary>
    private bool IsIgnoredPath(HttpContext context)
    {
        var requestPath = context.Request.Path.Value ?? "";
        var ignorePrefixes =
            configuration.GetSection("IgnoreRequestPrefix").Get<List<string>>() ?? [];

        return ignorePrefixes.Any(prefix =>
            requestPath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
        );
    }

    /// <summary>
    /// 延迟指定时间后返回 429 响应,增加攻击成本
    /// </summary>
    private async Task RespondBlockedAsync(HttpContext context, RateLimitResult result)
    {
        var delayMs = result.DelayMs;
        if (delayMs > 0 && !context.Response.HasStarted)
        {
            try
            {
                await Task.Delay(delayMs, context.RequestAborted);
            }
            catch (OperationCanceledException)
            {
                // 客户端已断开,直接结束
                return;
            }
        }

        if (context.Response.HasStarted)
        {
            logger.LogWarning(
                "IP {ClientIp} 限流响应时发现响应已开始,无法返回 429,路径: {RequestPath}",
                context.Request.GetIpAddress(),
                context.Request.Path
            );
            return;
        }

        context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.Response.ContentType = "application/json; charset=utf-8";

        var response = new
        {
            error = "IP_BLOCKED",
            message = result.Message,
            timestamp = DateTime.UtcNow,
            path = context.Request.Path.Value,
            delayMs,
        };
        await context.Response.WriteAsync(
            JsonSerializer.Serialize(
                response,
                new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
            )
        );

        logger.LogWarning(
            "IP {ClientIp} 处于封禁期,返回 429,路径: {RequestPath},延迟: {DelayMs}ms",
            context.Request.GetIpAddress(),
            context.Request.Path,
            delayMs
        );
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 的限流中间件,用于实现基于窗口期的 IP 访问限流功能。

主要功能

该中间件用于防止某个 IP 地址在短时间内发起过多请求,通过以下方式保护系统:

  1. 检测 IP 是否被封禁
  2. 对被封禁的 IP 延迟响应并返回 HTTP 429 状态码
  3. 增加恶意攻击者的攻击成本

代码结构分析

1. 构造函数(主构造器)

public class RateLimitMiddleware(
    RequestDelegate next,
    IConfiguration configuration,
    IWebHostEnvironment webHostEnvironment,
    IIpRateLimitService rateLimitService,
    ILogger<RateLimitMiddleware> logger
)

使用 C# 12 的主构造函数语法,依赖注入了以下服务:

  • next:下一个中间件委托
  • configuration:配置服务(读取忽略路径配置)
  • webHostEnvironment:环境信息(判断是否开发环境)
  • rateLimitService:限流核心服务
  • logger:日志记录器

2. 核心方法:InvokeAsync

处理每个 HTTP 请求的主要逻辑:

白名单逻辑(不参与限流)

if (
    context.User.Identity?.IsAuthenticated == true  // 已登录用户
    || webHostEnvironment.IsDevelopment()           // 开发环境
    || IsIgnoredPath(context)                       // 配置的忽略路径
)

获取客户端 IP

var clientIp = context.Request.GetIpAddress();

通过扩展方法获取真实客户端 IP(可能考虑代理/负载均衡)

限流检测

var result = await rateLimitService.CheckLimitStatusAsync(clientIp, context.RequestAborted);

调用限流服务检查该 IP 的状态

处理结果

  • 允许通过await next(context) 继续执行后续中间件
  • 被封禁:调用 RespondBlockedAsync 返回 429 响应

3. IsIgnoredPath 方法

检查当前请求路径是否在忽略列表中:

var ignorePrefixes = configuration.GetSection("IgnoreRequestPrefix").Get<List<string>>();

从配置文件读取忽略的路径前缀(如 /health/metrics 等),这些路径不参与限流。


4. RespondBlockedAsync 方法

对被封禁的 IP 进行响应处理:

关键设计:延迟响应

await Task.Delay(delayMs, context.RequestAborted);
  • 目的:增加攻击者的时间成本,降低攻击效率
  • 可取消:当客户端断开连接时捕获 OperationCanceledException

返回 429 状态码

context.Response.StatusCode = StatusCodes.Status429TooManyRequests;

JSON 响应体

{
  "error": "IP_BLOCKED",
  "message": "具体限流原因",
  "timestamp": "2024-01-01T00:00:00Z",
  "path": "/api/data",
  "delayMs": 5000
}

防御性检查

if (context.Response.HasStarted) { ... }

检测响应是否已经开始发送(避免重复写入响应头)


配置示例

{
  "IgnoreRequestPrefix": [
    "/health",
    "/metrics",
    "/swagger"
  ]
}

设计亮点

  1. 白名单机制:已认证用户、开发环境、特定路径不受限流影响
  2. 延迟惩罚:被封禁的请求故意延迟响应,增加 DDoS 攻击成本
  3. 优雅处理取消:支持客户端中断连接的场景
  4. 详细日志:记录封禁事件便于监控和排查
  5. 标准化响应:返回符合 REST 规范的 429 状态码和 JSON 错误信息

应用场景

  • 防止暴力破解(登录接口)
  • 防止 API 滥用
  • 防止爬虫过度抓取
  • DDoS 攻击缓解

这是一个生产级的限流中间件实现,具有良好的可扩展性和健壮性。

评论加载中...