网站首页 网站源码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ZiggyCreatures.Caching.Fusion;
namespace Dpz.Core.Infrastructure.RateLimiting;
/// <summary>
/// 基于行为的IP限流服务实现
/// 专注于限制可疑行为,不限制正常请求频率
/// </summary>
public class IpRateLimitService(
IFusionCache fusionCache,
ILogger<IpRateLimitService> logger,
IConfiguration configuration,
RateLimitConfig? config = null
) : IIpRateLimitService
{
private readonly RateLimitConfig _config = config ?? new RateLimitConfig();
private const string CacheKeyPrefix = "RateLimit:Block:";
private const string WhiteListConfigurationKey = "IpRateLimitWhiteList";
// 缓存白名单,避免每次都读取配置
private HashSet<string>? _cachedWhiteList;
private DateTime _whiteListCacheTime = DateTime.MinValue;
private readonly TimeSpan _whiteListCacheExpiry = TimeSpan.FromMinutes(5); // 5分钟缓存
/// <summary>
/// 检查IP是否被限流
/// 只检查状态,不记录任何访问信息
/// </summary>
public async Task<RateLimitResult> CheckLimitStatusAsync(
string ip,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrEmpty(ip))
{
return RateLimitResult.Allow();
}
// 检查白名单
if (IsWhitelisted(ip))
{
return RateLimitResult.Allow();
}
var record = await GetBlockRecordAsync(ip);
if (record == null)
{
// 没有拦截记录,允许通过
return RateLimitResult.Allow();
}
var now = DateTime.UtcNow;
// 检查是否在限流期内
if (record.BlockedUntil.HasValue && now < record.BlockedUntil.Value)
{
var remainingTime = record.BlockedUntil.Value - now;
var delayMs = CalculateBlockedDelay(record);
// 根据配置决定是否记录详细日志
if (_config.EnableVerboseLogging)
{
logger.LogInformation(
"IP {Ip} 仍在限流期内,剩余时间: {RemainingTime},延迟: {DelayMs}ms",
ip,
remainingTime,
delayMs
);
}
return RateLimitResult.Block(
$"IP blocked for suspicious behavior, remaining time: {remainingTime:mm\\:ss}",
delayMs
);
}
// 限流期已过,允许通过
return RateLimitResult.Allow();
}
/// <summary>
/// 记录IP被拦截事件
/// 当安全中间件检测到可疑行为时调用
/// </summary>
public async Task RecordBlockAsync(string ip, string? reason = null)
{
if (string.IsNullOrEmpty(ip))
{
return;
}
// 检查白名单
if (IsWhitelisted(ip))
{
return;
}
var record = await GetOrCreateBlockRecordAsync(ip);
var now = DateTime.UtcNow;
// 检查是否需要重置窗口
if (now - record.WindowStart > TimeSpan.FromMinutes(_config.WindowSizeMinutes))
{
ResetWindow(record, now);
}
// 记录拦截事件
record.BlockCount++;
record.LastBlockTime = now;
logger.LogWarning(
"IP {Ip} 被拦截,原因: {Reason},当前窗口拦截次数: {BlockCount}/{MaxBlocks}",
ip,
reason ?? "未指定",
record.BlockCount,
_config.MaxBlocksPerWindow
);
// 检查是否需要限流
if (record.BlockCount >= _config.MaxBlocksPerWindow)
{
record.BlockedUntil = now.AddMinutes(_config.BlockDurationMinutes);
logger.LogWarning(
"IP {Ip} 因频繁可疑行为被限流 {Duration} 分钟,拦截次数: {BlockCount}",
ip,
_config.BlockDurationMinutes,
record.BlockCount
);
}
// 更新缓存
await UpdateBlockRecordAsync(ip, record);
}
/// <summary>
/// 获取IP拦截记录
/// </summary>
private async Task<IpAccessRecord?> GetBlockRecordAsync(string ip)
{
var cacheKey = GetCacheKey(ip);
var result = await fusionCache.TryGetAsync<IpAccessRecord>(cacheKey);
return result.HasValue ? result.Value : null;
}
/// <summary>
/// 获取或创建IP拦截记录
/// </summary>
private async Task<IpAccessRecord> GetOrCreateBlockRecordAsync(string ip)
{
var cacheKey = GetCacheKey(ip);
var expiration = TimeSpan.FromMinutes(
_config.WindowSizeMinutes + _config.CleanupIntervalMinutes
);
var record = await fusionCache.GetOrSetAsync<IpAccessRecord>(
cacheKey,
(_, _) =>
Task.FromResult(
new IpAccessRecord
{
WindowStart = DateTime.UtcNow,
LastBlockTime = DateTime.UtcNow,
BlockCount = 0,
}
),
options =>
{
options.SetDuration(expiration);
// 针对限流服务优化:适中的抖动时间,平衡性能和容错
options.JitterMaxDuration = TimeSpan.FromMilliseconds(500);
}
);
return record;
}
/// <summary>
/// 更新IP拦截记录到缓存
/// </summary>
private async Task UpdateBlockRecordAsync(string ip, IpAccessRecord record)
{
var cacheKey = GetCacheKey(ip);
var expiration = TimeSpan.FromMinutes(
_config.WindowSizeMinutes + _config.CleanupIntervalMinutes
);
await fusionCache.SetAsync(
cacheKey,
record,
options =>
{
options.SetDuration(expiration);
// 针对限流服务优化:适中的抖动时间,平衡性能和容错
options.JitterMaxDuration = TimeSpan.FromMilliseconds(500);
}
);
}
/// <summary>
/// 构建缓存键
/// </summary>
private static string GetCacheKey(string ip)
{
return $"{CacheKeyPrefix}{ip}";
}
/// <summary>
/// 重置时间窗口
/// </summary>
private static void ResetWindow(IpAccessRecord record, DateTime now)
{
record.WindowStart = now;
record.BlockCount = 0;
// 保留限流状态 BlockedUntil
}
/// <summary>
/// 计算被限流时的延迟时间
/// 基于拦截次数进行渐进式延迟,让攻击者"不爽"
/// </summary>
private int CalculateBlockedDelay(IpAccessRecord record)
{
// 基础延迟 + 根据拦截次数递增
var baseDelay = _config.BlockedDelayBaseMs;
var extraDelay = Math.Min(
record.BlockCount * 1000, // 每次拦截增加1秒
_config.BlockedDelayMaxMs - baseDelay
);
return Math.Min(baseDelay + extraDelay, _config.BlockedDelayMaxMs);
}
/// <summary>
/// 获取IP白名单(带缓存)
/// </summary>
private HashSet<string> GetWhiteList()
{
var now = DateTime.UtcNow;
// 检查缓存是否有效
if (_cachedWhiteList != null && now - _whiteListCacheTime < _whiteListCacheExpiry)
{
return _cachedWhiteList;
}
try
{
// 尝试从配置中读取白名单
var configSection = configuration.GetSection(WhiteListConfigurationKey);
var whiteListArray = configSection.Get<string[]>() ?? [];
var whiteList = new HashSet<string>(whiteListArray, StringComparer.OrdinalIgnoreCase);
// 只在白名单条目数量变化时记录,避免重复日志
if (_cachedWhiteList == null || _cachedWhiteList.Count != whiteList.Count)
{
logger.LogInformation("IP白名单已更新,共 {Count} 个条目", whiteList.Count);
}
// 更新缓存
_cachedWhiteList = whiteList;
_whiteListCacheTime = now;
return whiteList;
}
catch (Exception e)
{
logger.LogError(e, "获取IP白名单失败,使用空白名单");
// 失败时使用空集合,但不缓存(下次继续尝试)
return [];
}
}
/// <summary>
/// 检查IP是否在白名单中(支持CIDR)
/// </summary>
private bool IsWhitelisted(string ip)
{
if (string.IsNullOrEmpty(ip))
{
return false;
}
var whiteList = GetWhiteList();
if (whiteList.Count == 0)
{
return false;
}
// 先检查精确匹配(最快)
if (whiteList.Contains(ip))
{
logger.LogDebug("IP {Ip} 匹配白名单精确条目", ip);
return true;
}
// 解析IP地址(只解析一次)
if (!IPAddress.TryParse(ip, out var ipAddress))
{
logger.LogWarning("无效的IP地址格式: {Ip}", ip);
return false;
}
// 检查CIDR匹配(按地址族分组优化)
return whiteList.Any(entry => entry.Contains('/') && IsIpInRange(ipAddress, entry));
}
/// <summary>
/// 检查IP地址是否在指定的CIDR范围内
/// </summary>
private static bool IsIpInRange(IPAddress ip, string cidr)
{
try
{
// 如果不是CIDR格式,直接返回false
if (string.IsNullOrEmpty(cidr) || !cidr.Contains('/'))
{
return false;
}
var parts = cidr.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
return false;
}
var networkAddress = parts[0].Trim();
if (!int.TryParse(parts[1].Trim(), out var prefixLength))
{
return false;
}
if (!IPAddress.TryParse(networkAddress, out var networkIp))
{
return false;
}
// 确保两个IP地址是同一类型(IPv4或IPv6)
if (networkIp.AddressFamily != ip.AddressFamily)
{
return false;
}
// 验证前缀长度的有效性
var maxPrefixLength =
networkIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? 32 : 128;
if (prefixLength < 0 || prefixLength > maxPrefixLength)
{
return false;
}
var networkBytes = networkIp.GetAddressBytes();
var targetBytes = ip.GetAddressBytes();
var maskBytes = GetNetworkMask(prefixLength, networkBytes.Length);
// 比较网络部分是否匹配
return !networkBytes
.Where((x, y) => (x & maskBytes[y]) != (targetBytes[y] & maskBytes[y]))
.Any();
}
catch
{
// 任何异常都认为不匹配
return false;
}
}
/// <summary>
/// 根据前缀长度生成网络掩码
/// </summary>
private static byte[] GetNetworkMask(int prefixLength, int addressBytesLength)
{
var maskBytes = new byte[addressBytesLength];
var fullBytes = prefixLength / 8;
var remainingBits = prefixLength % 8;
// 设置完整的字节
for (var i = 0; i < fullBytes && i < maskBytes.Length; i++)
{
maskBytes[i] = 0xFF;
}
// 设置剩余的位
if (fullBytes < maskBytes.Length && remainingBits > 0)
{
maskBytes[fullBytes] = (byte)(0xFF << (8 - remainingBits));
}
return maskBytes;
}
}
上述代码实现了一个基于行为的IP限流服务,主要用于限制可疑行为的IP地址,而不限制正常请求的频率。以下是代码的主要功能和结构的详细解释:
IP限流检查:
CheckLimitStatusAsync 方法用于检查给定IP地址是否被限流。如果IP在白名单中,或者没有拦截记录,则允许通过;如果在限流期内,则返回被限流的状态和剩余时间。记录拦截事件:
RecordBlockAsync 方法用于记录IP地址的拦截事件。当检测到可疑行为时调用此方法。它会更新拦截记录,包括拦截次数和最后拦截时间,并在达到最大拦截次数时设置限流状态。缓存管理:
IFusionCache 进行缓存管理,存储IP的拦截记录,以提高性能并减少对存储的频繁访问。白名单管理:
GetWhiteList 方法获取IP白名单,并使用缓存来避免频繁读取配置。白名单中的IP地址不会受到限流。CIDR支持:
限流策略:
日志记录:
ILogger 记录相关的操作和状态,包括拦截事件、白名单更新等,便于后续的审计和调试。构造函数:
私有方法:
GetBlockRecordAsync:获取指定IP的拦截记录。GetOrCreateBlockRecordAsync:获取或创建IP的拦截记录。UpdateBlockRecordAsync:更新IP的拦截记录到缓存。GetCacheKey:生成缓存键。ResetWindow:重置时间窗口。CalculateBlockedDelay:计算被限流时的延迟时间。GetWhiteList:获取IP白名单并进行缓存。IsWhitelisted:检查IP是否在白名单中。IsIpInRange:检查IP地址是否在指定的CIDR范围内。GetNetworkMask:根据前缀长度生成网络掩码。这个IP限流服务的设计旨在保护系统免受可疑行为的影响,同时允许正常用户的请求不受限制。通过使用缓存和白名单机制,服务能够高效地处理请求并减少对存储的依赖。
