需求:单IP接口请求间隔不能小于120S
实现方案:①应用程序内存中记录各IP访问时间 ②使用中间件Redis记录各IP访问时间
.net core 版本:.net 8
方案一代码:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Concurrent;
namespace MESNetCoreWebApi.Attributes
{
public class RequestIntervalAttribute : ActionFilterAttribute
{
private static readonly ConcurrentDictionary<string, DateTime> LastRequestTimes = new();
private readonly int _intervalSeconds;
public RequestIntervalAttribute(int intervalSeconds)
{
_intervalSeconds = intervalSeconds;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var clientKey = GetClientKey(context);
if (LastRequestTimes.TryGetValue(clientKey, out var lastRequestTime))
{
var elapsed = DateTime.UtcNow - lastRequestTime;
if (elapsed.TotalSeconds < _intervalSeconds)
{
var waitTime = _intervalSeconds - (int)elapsed.TotalSeconds;
context.Result = new ObjectResult(new
{
Code = 999,
Msg = $"请求过于频繁,请等待 {waitTime} 秒后再试",
//retryAfter = waitTime
})
{
StatusCode = StatusCodes.Status429TooManyRequests
};
return;
}
}
LastRequestTimes[clientKey] = DateTime.UtcNow;
base.OnActionExecuting(context);
}
private string GetClientKey(ActionExecutingContext context)
{
// 使用IP+UserAgent作为客户端标识
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = context.HttpContext.Request.Headers["User-Agent"].ToString();
return $"{ip}_{userAgent}";
}
}
}
方案二代码:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using StackExchange.Redis;
using System;
namespace MESNetCoreWebApi.Attributes
{
public class RedisRequestIntervalAttribute : ActionFilterAttribute
{
private readonly int _intervalSeconds;
private readonly string _rateLimitPrefix = "RateLimit:";
public RedisRequestIntervalAttribute(int intervalSeconds)
{
_intervalSeconds = intervalSeconds;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 获取 Redis 实例,通过依赖注入
var redis = context.HttpContext.RequestServices.GetRequiredService<IConnectionMultiplexer>();
var _redis = redis.GetDatabase(); // 获取默认数据库(0号数据库)
var clientKey = GetClientKey(context);
var redisKey = _rateLimitPrefix + clientKey;
// 获取上次请求时间
var lastRequestTimeStr = await _redis.StringGetAsync(redisKey);
if (!lastRequestTimeStr.IsNullOrEmpty &&
DateTime.TryParse(lastRequestTimeStr, out var lastRequestTime))
{
var elapsed = DateTime.Now - lastRequestTime;
if (elapsed.TotalSeconds < _intervalSeconds)
{
var waitTime = _intervalSeconds - (int)elapsed.TotalSeconds;
context.Result = new ObjectResult(new
{
Code = 999,
Msg = $"请求过于频繁,请等待 {waitTime} 秒后再试",
RetryAfter = waitTime
})
{
StatusCode = StatusCodes.Status429TooManyRequests
};
return;
}
}
// 更新最后请求时间并设置过期时间
await _redis.StringSetAsync(
redisKey,
DateTime.Now.ToString(),
TimeSpan.FromSeconds(_intervalSeconds * 2)); // 设置稍长的过期时间
await next();
}
private string GetClientKey(ActionExecutingContext context)
{
// 使用IP+UserAgent+请求路径作为客户端标识
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = context.HttpContext.Request.Headers.UserAgent.ToString();
var path = context.HttpContext.Request.Path;
return $"{ip}_{userAgent}_{path}";
}
}
}
如果使用应用程序内存记录,重启服务会丢失记录,建议使用Redis