mcpField
R
rate-limiting

rate-limiting

github.com/wshaddix安全8685 次安装35 周前更新
代码仓库

关于 rate-limiting

面向 ASP.NET Core 开发者的请求限流实践指南,涵盖内置中间件、分布式 Redis 限流等模式,并指出常见反模式。

主要功能

  1. 01使用 .NET 内置限流中间件
  2. 02配置端点级策略
  3. 03基于 Redis 的分布式限流
  4. 04按用户身份限流
  5. 05处理代理后的客户端识别

适用场景

  1. 01为不同 API 端点设置限额
  2. 02在多台服务器间实施限流
  3. 03按已认证用户限制请求

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill rate-limiting

安装后在 Claude Code 中输入/rate-limiting确认已加载。

详细介绍

原因

速率限制可以保护应用程序免受滥用,确保资源得到公平使用,并防止流量激增时发生级联故障。如果没有适当的速率限制,恶意或意外的大量请求可能会使 API 不堪重负,导致性能下降或服务中断。以下模式为 ASP.NET Core 应用程序提供了可用于生产环境的请求节流方案。

模式

模式 1:内置速率限制中间件(.NET 7+)

在常见场景中使用内置的 Microsoft.AspNetCore.RateLimiting 中间件。

// Program.cs - Basic rate limiting configuration
builder.Services.AddRateLimiter(options =>
{
    // Global rate limit for all requests
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        httpContext =>
        {
            var clientId = httpContext.User.Identity?.Name ?? 
                          httpContext.Connection.RemoteIpAddress?.ToString() ?? 
                          "anonymous";
            
            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: clientId,
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 100,
                    Window = TimeSpan.FromMinutes(1),
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 2
                });
        });

    // Named policies for different endpoints
    options.AddFixedWindowLimiter("login", opt =>
    {
        opt.PermitLimit = 5;
        opt.Window = TimeSpan.FromMinutes(5);
        opt.QueueLimit = 0; // Don't queue login requests
    });

    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 1000;
        opt.Window = TimeSpan.FromMinutes(1);
    });

    options.AddSlidingWindowLimiter("strict", opt =>
    {
        opt.PermitLimit = 10;
        opt.Window = TimeSpan.FromSeconds(10);
        opt.SegmentsPerWindow = 2;
    });

    options.AddTokenBucketLimiter("burst", opt =>
    {
        opt.TokenLimit = 100;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 5;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        opt.TokensPerPeriod = 20;
        opt.AutoReplenishment = true;
    });

    options.AddConcurrencyLimiter("concurrent", opt =>
    {
        opt.PermitLimit = 10;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 5;
    });

    // Custom rejection response
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers.Append("Retry-After", "60");
        
        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            Error = "Rate limit exceeded. Please try again later.",
            RetryAfter = 60
        }, token);
    };
});

// Middleware placement (must be after UseRouting, before UseEndpoints)
var app = builder.Build();
app.UseRouting();
app.UseRateLimiter(); // Enable rate limiting
app.MapControllers();
app.MapRazorPages();
模式 2:按端点设置速率限制

使用特性或端点配置,为不同端点应用不同的速率限制。

// Using EnableRateLimiting attribute on controllers
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("api")] // Use named policy
public class ProductsController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        // Limited by "api" policy (1000 requests/minute)
        return Ok();
    }

    [HttpPost]
    [EnableRateLimiting("strict")] // Override with stricter policy
    public async Task<IActionResult> Create([FromBody] ProductDto dto)
    {
        // Limited by "strict" policy (10 requests/10 seconds)
        return Created();
    }
}

// Razor Pages with rate limiting
public class LoginModel : PageModel
{
    // Page is rate limited via attribute
    [RateLimitPolicy("login")]
    public async Task<IActionResult> OnPostAsync()
    {
        // Login logic - protected by login policy (5 attempts per 5 minutes)
    }
}

// Endpoint-specific configuration in Program.cs
app.MapPost("/api/login", async (LoginRequest request) =>
{
    // Login logic
})
.AddEndpointFilter<RateLimitEndpointFilter>()
.RequireRateLimiting("login");

// Disable rate limiting for specific endpoints
app.MapGet("/health", () => Results.Ok())
    .DisableRateLimiting();
模式 3:基于 Redis 的分布式速率限制

在分布式/多服务端环境中使用 Redis 进行速率限制。

// Redis rate limiting configuration
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        httpContext =>
        {
            var clientId = GetClientIdentifier(httpContext);
            
            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: clientId,
                factory: partitionKey => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 100,
                    Window = TimeSpan.FromMinutes(1)
                });
        });
});

// Custom distributed rate limiter using Redis
public class RedisRateLimiter : IRateLimiter
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<RedisRateLimiter> _logger;

    public RedisRateLimiter(IConnectionMultiplexer redis, ILogger<RedisRateLimiter> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    public async Task<RateLimitResult> CheckLimitAsync(
        string key, 
        int limit, 
        TimeSpan window)
    {
        var db = _redis.GetDatabase();
        var redisKey = $"ratelimit:{key}";
        
        // Lua script for atomic check-and-increment
        var script = @"
            local current = redis.call('GET', KEYS[1])
            if current == false then
                current = 0
            end
            if tonumber(current) < tonumber(ARGV[1]) then
                redis.call('INCR', KEYS[1])
                redis.call('EXPIRE', KEYS[1], ARGV[2])
                return {1, tonumber(current) + 1, tonumber(ARGV[1])}
            else
                local ttl = redis.call('TTL', KEYS[1])
                return {0, tonumber(current), tonumber(ARGV[1]), ttl}
            end";

        var result = await db.ScriptEvaluateAsync(script,
            new RedisKey[] { redisKey },
            new RedisValue[] { limit, window.TotalSeconds });

        var values = (RedisResult[])result!;
        var allowed = (bool)values[0];
        var current = (int)values[1];
        var limitValue = (int)values[2];
        var retryAfter = allowed ? 0 : (int)values[3];

        return new RateLimitResult(
            Allowed: allowed,
            Current: current,
            Limit: limitValue,
            RetryAfter: retryAfter);
    }
}

public record RateLimitResult(bool Allowed, int Current, int Limit, int RetryAfter);

// Custom rate limiting middleware
public class DistributedRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RedisRateLimiter _rateLimiter;
    private readonly ILogger<DistributedRateLimitMiddleware> _logger;

    public DistributedRateLimitMiddleware(
        RequestDelegate next,
        RedisRateLimiter rateLimiter,
        ILogger<DistributedRateLimitMiddleware> logger)
    {
        _next = next;
        _rateLimiter = rateLimiter;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientId = GetClientIdentifier(context);
        var path = context.Request.Path.Value ?? "";
        
        // Different limits for different paths
        var (limit, window) = GetLimitForPath(path);
        
        var result = await _rateLimiter.CheckLimitAsync(
            $"{clientId}:{path}", 
            limit, 
            window);

        // Add rate limit headers
        AddRateLimitHeaders(context.Response, result);

        if (!result.Allowed)
        {
            _logger.LogWarning(
                "Rate limit exceeded for {ClientId} on {Path}",
                clientId, path);

            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers.Append("Retry-After", result.RetryAfter.ToString());
            
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "Rate limit exceeded",
                RetryAfter = result.RetryAfter,
                Limit = result.Limit,
                Window = window.TotalSeconds
            });
            
            return;
        }

        await _next(context);
    }

    private static (int Limit, TimeSpan Window) GetLimitForPath(string path)
    {
        if (path.StartsWith("/api/login"))
            return (5, TimeSpan.FromMinutes(5));
        if (path.StartsWith("/api/"))
            return (1000, TimeSpan.FromMinutes(1));
        
        return (100, TimeSpan.FromMinutes(1));
    }

    private static void AddRateLimitHeaders(HttpResponse response, RateLimitResult result)
    {
        response.Headers.Append("X-RateLimit-Limit", result.Limit.ToString());
        response.Headers.Append("X-RateLimit-Remaining", (result.Limit - result.Current).ToString());
    }
}
模式 4:基于用户的速率限制

根据经过身份验证的用户身份实施速率限制。

// User-based rate limiter
public class UserBasedRateLimiter
{
    private readonly IRateLimiter _rateLimiter;
    private readonly IUserService _userService;

    public UserBasedRateLimiter(IRateLimiter rateLimiter, IUserService userService)
    {
        _rateLimiter = rateLimiter;
        _userService = userService;
    }

    public async Task<bool> CheckUserLimitAsync(HttpContext context)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        if (string.IsNullOrEmpty(userId))
        {
            // Fall back to IP-based limiting for anonymous users
            return await CheckAnonymousLimitAsync(context);
        }

        // Get user's subscription tier
        var user = await _userService.GetUserAsync(userId);
        var (limit, window) = GetLimitForTier(user?.SubscriptionTier);

        var key = $"user:{userId}";
        var result = await _rateLimiter.CheckLimitAsync(key, limit, window);

        AddRateLimitHeaders(context.Response, result);
        
        return result.Allowed;
    }

    private async Task<bool> CheckAnonymousLimitAsync(HttpContext context)
    {
        var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var key = $"ip:{ipAddress}";
        
        // Stricter limits for anonymous users
        var result = await _rateLimiter.CheckLimitAsync(key, 30, TimeSpan.FromMinutes(1));
        
        AddRateLimitHeaders(context.Response, result);
        
        return result.Allowed;
    }

    private static (int Limit, TimeSpan Window) GetLimitForTier(SubscriptionTier? tier)
    {
        return tier switch
        {
            SubscriptionTier.Enterprise => (10000, TimeSpan.FromMinutes(1)),
            SubscriptionTier.Pro => (1000, TimeSpan.FromMinutes(1)),
            SubscriptionTier.Basic => (100, TimeSpan.FromMinutes(1)),
            _ => (50, TimeSpan.FromMinutes(1)) // Free tier
        };
    }
}

// Middleware integration
public class UserRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly UserBasedRateLimiter _rateLimiter;

    public UserRateLimitMiddleware(RequestDelegate next, UserBasedRateLimiter rateLimiter)
    {
        _next = next;
        _rateLimiter = rateLimiter;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!await _rateLimiter.CheckUserLimitAsync(context))
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "Rate limit exceeded",
                UpgradeUrl = "/pricing"
            });
            return;
        }

        await _next(context);
    }
}

// Razor Page with tier-based limiting
public class ApiDashboardModel : PageModel
{
    private readonly IUserRateLimitService _rateLimitService;

    public int CurrentUsage { get; set; }
    public int MonthlyLimit { get; set; }

    public async Task OnGetAsync()
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        
        var usage = await _rateLimitService.GetMonthlyUsageAsync(userId);
        CurrentUsage = usage.Current;
        MonthlyLimit = usage.Limit;
    }
}
模式 5:基于客户端标识的速率限制

处理各种客户端标识场景,包括代理和负载均衡器。

public static class ClientIdentifierHelper
{
    public static string GetClientIdentifier(HttpContext context)
    {
        // 1. Try authenticated user first
        var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            return $"user:{userId}";
        }

        // 2. Try API key
        var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
        if (!string.IsNullOrEmpty(apiKey))
        {
            return $"apikey:{apiKey}";
        }

        // 3. Get IP address (handling proxies)
        var ip = GetClientIpAddress(context);
        return $"ip:{ip}";
    }

    public static string GetClientIpAddress(HttpContext context)
    {
        // Check X-Forwarded-For header (when behind load balancer/proxy)
        var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        if (!string.IsNullOrEmpty(forwardedFor))
        {
            // Take the first IP if multiple are present
            var ips = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
            if (ips.Length > 0)
            {
                return ips[0].Trim();
            }
        }

        // Check X-Real-IP header
        var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault();
        if (!string.IsNullOrEmpty(realIp))
        {
            return realIp;
        }

        // Fall back to connection IP
        return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    }

    public static bool IsTrustedProxy(HttpContext context, IEnumerable<string> trustedProxies)
    {
        var remoteIp = context.Connection.RemoteIpAddress;
        return remoteIp != null && trustedProxies.Any(proxy =>
        {
            if (IPAddress.TryParse(proxy, out var trustedIp))
            {
                return remoteIp.Equals(trustedIp);
            }
            return false;
        });
    }
}

// Configuration for forwarded headers (Program.cs)
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

// Use forwarded headers middleware
app.UseForwardedHeaders();

反模式

// ❌ BAD: Same limits for all endpoints
options.AddFixedWindowLimiter("default", opt =>
{
    opt.PermitLimit = 100;
    opt.Window = TimeSpan.FromMinutes(1);
});
// Applied to everything - login endpoints need stricter limits!

// ✅ GOOD: Different policies for different endpoints
options.AddFixedWindowLimiter("login", opt =>
{
    opt.PermitLimit = 5; // Strict for authentication
    opt.Window = TimeSpan.FromMinutes(5);
});

options.AddFixedWindowLimiter("api", opt =>
{
    opt.PermitLimit = 1000; // Generous for API
    opt.Window = TimeSpan.FromMinutes(1);
});

// ❌ BAD: No headers indicating rate limit status
// Clients can't track their usage

// ✅ GOOD: Include rate limit headers
context.Response.Headers.Append("X-RateLimit-Limit", limit.ToString());
context.Response.Headers.Append("X-RateLimit-Remaining", remaining.ToString());
context.Response.Headers.Append("X-RateLimit-Reset", resetTime.ToString());

// ❌ BAD: Wrong middleware order
app.UseRateLimiter();
app.UseAuthentication();
// Can't identify users if auth hasn't run yet!

// ✅ GOOD: Rate limiter after authentication
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

// ❌ BAD: Not handling rate limit in-memory only
// Won't work across multiple servers
var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
{
    PermitLimit = 100,
    Window = TimeSpan.FromMinutes(1)
});

// ✅ GOOD: Use distributed storage for multi-server
typeof(DistributedCacheRateLimiter)

// ❌ BAD: No fallback when rate limiter fails
public async Task<bool> CheckLimit(string key)
{
    var result = await _redis.CheckLimitAsync(key); // If Redis fails, whole app fails!
    return result.Allowed;
}

// ✅ GOOD: Graceful degradation
public async Task<bool> CheckLimit(string key)
{
    try
    {
        var result = await _redis.CheckLimitAsync(key);
        return result.Allowed;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Rate limit check failed, allowing request");
        return true; // Fail open
    }
}

// ❌ BAD: Blocking on rate limit check
public IActionResult GetData()
{
    var allowed = CheckLimitAsync().Result; // Blocks thread!
    if (!allowed) return StatusCode(429);
    // ...
}

// ✅ GOOD: Async rate limiting
public async Task<IActionResult> GetDataAsync()
{
    var allowed = await CheckLimitAsync();
    if (!allowed) return StatusCode(429);
    // ...
}

// ❌ BAD: Logging every blocked request at Error level
// Creates log spam during attacks

// ✅ GOOD: Log at appropriate level with sampling
_logger.LogWarning("Rate limit exceeded for {ClientId}", clientId);

// Or use metrics instead
_metrics.RecordRateLimitHit(clientId);

参考资料

评论

支持 Markdown