using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Medallion.Threading;
using Microsoft.Extensions.Logging;
using ZiggyCreatures.Caching.Fusion;
namespace Dpz.Core.Infrastructure.RateLimiting;
/// <summary>
/// 窗口期 IP 限流服务实现
/// 可疑事件在窗口内计数,超过阈值后封禁该 IP 一段时间
/// </summary>
public class IpRateLimitService(
IFusionCache fusionCache,
IDistributedLockProvider distributedLockProvider,
ILogger<IpRateLimitService> logger,
RateLimitConfig config
) : IIpRateLimitService
{
private const string CacheKeyPrefix = "RateLimit:Window:";
/// <summary>
/// 封禁中的 IP 索引,供管理接口查询
/// </summary>
private const string BlockedIndexKey = "RateLimit:BlockedIndex";
/// <summary>
/// 封禁索引读写锁
/// </summary>
private const string BlockedIndexLockKey = "RateLimit:BlockedIndex:Lock";
/// <summary>
/// 封禁索引有效期,过期 IP 由查询时惰性清理
/// </summary>
private static readonly TimeSpan BlockedIndexDuration = TimeSpan.FromDays(7);
/// <summary>
/// 检查 IP 是否处于封禁期
/// 缓存异常时降级放行,避免限流组件拖垮整个应用
/// </summary>
public async Task<RateLimitResult> CheckLimitStatusAsync(
string ip,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrEmpty(ip))
{
return RateLimitResult.Allow();
}
try
{
var record = await GetRecordAsync(ip, cancellationToken);
if (record is not { BlockedUntil: { } blockedUntil })
{
return RateLimitResult.Allow();
}
var now = DateTime.UtcNow;
if (now < blockedUntil)
{
var remaining = blockedUntil - now;
logger.LogInformation(
"IP {Ip} 仍在封禁期,剩余 {Remaining},延迟 {DelayMs}ms",
ip,
remaining,
config.BlockDelaySeconds * 1000
);
return RateLimitResult.Block(
$"IP blocked for suspicious behavior, remaining time: {remaining:mm\\:ss}",
config.BlockDelaySeconds * 1000
);
}
return RateLimitResult.Allow();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
// 缓存故障时降级放行,保证服务可用性
logger.LogWarning(ex, "检查 IP {Ip} 限流状态失败,降级放行", ip);
return RateLimitResult.Allow();
}
}
/// <summary>
/// 记录一次可疑事件,窗口内事件数达到阈值后封禁
/// </summary>
public async Task RecordEventAsync(
string ip,
string? reason = null,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrEmpty(ip))
{
return;
}
try
{
await using (
await distributedLockProvider
.CreateLock(GetLockKey(ip))
.AcquireAsync(cancellationToken: cancellationToken)
)
{
var record = await GetRecordForUpdateAsync(ip, cancellationToken) ?? CreateRecord();
var now = DateTime.UtcNow;
// 窗口过期则重置计数,开始新的窗口
if (now - record.WindowStart > TimeSpan.FromMinutes(config.WindowMinutes))
{
ResetWindow(record, now);
}
record.EventCount++;
record.LastEventTime = now;
logger.LogWarning(
"IP {Ip} 触发可疑事件,原因: {Reason},当前窗口事件数: {EventCount}/{MaxEvents}",
ip,
reason ?? "未指定",
record.EventCount,
config.MaxEvents
);
// 达到阈值则封禁
if (record.EventCount >= config.MaxEvents)
{
record.BlockedUntil = now.AddMinutes(config.BlockMinutes);
logger.LogWarning(
"IP {Ip} 窗口内事件达到阈值,封禁 {BlockMinutes} 分钟,事件数: {EventCount}",
ip,
config.BlockMinutes,
record.EventCount
);
await AddToBlockedIndexAsync(ip, cancellationToken);
}
await SetRecordAsync(
ip,
record,
config.WindowMinutes + config.BlockMinutes,
cancellationToken
);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
// 记录失败不影响请求本身
logger.LogWarning(ex, "记录 IP {Ip} 可疑事件失败,原因: {Reason}", ip, reason);
}
}
/// <summary>
/// 手动封禁指定 IP,立即写入封禁状态与索引
/// </summary>
public async Task BlockIpAsync(
string ip,
int minutes,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrEmpty(ip) || minutes <= 0)
{
return;
}
try
{
await using (
await distributedLockProvider
.CreateLock(GetLockKey(ip))
.AcquireAsync(cancellationToken: cancellationToken)
)
{
var record = await GetRecordForUpdateAsync(ip, cancellationToken) ?? CreateRecord();
record.BlockedUntil = DateTime.UtcNow.AddMinutes(minutes);
await SetRecordAsync(ip, record, config.WindowMinutes + minutes, cancellationToken);
}
await AddToBlockedIndexAsync(ip, cancellationToken);
logger.LogWarning("手动封禁 IP {Ip},时长 {Minutes} 分钟", ip, minutes);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
logger.LogWarning(ex, "手动封禁 IP {Ip} 失败", ip);
throw;
}
}
/// <summary>
/// 手动解封指定 IP:删除窗口记录并移出封禁索引
/// </summary>
public async Task UnblockIpAsync(string ip, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(ip))
{
return;
}
try
{
await using (
await distributedLockProvider
.CreateLock(GetLockKey(ip))
.AcquireAsync(cancellationToken: cancellationToken)
)
{
await fusionCache.RemoveAsync(
GetCacheKey(ip),
new FusionCacheEntryOptions
{
AllowBackgroundDistributedCacheOperations = false,
IsFailSafeEnabled = false,
ReThrowDistributedCacheExceptions = true,
},
token: cancellationToken
);
}
await RemoveFromBlockedIndexAsync(ip, cancellationToken);
logger.LogWarning("手动解封 IP {Ip}", ip);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
logger.LogWarning(ex, "手动解封 IP {Ip} 失败", ip);
throw;
}
}
/// <summary>
/// 查询当前处于封禁期的 IP 列表
/// 顺带清理索引中已过期的 IP
/// </summary>
public async Task<IList<BlockedIpInfo>> GetBlockedIpsAsync(
CancellationToken cancellationToken = default
)
{
try
{
await using (
await distributedLockProvider
.CreateLock(BlockedIndexLockKey)
.AcquireAsync(cancellationToken: cancellationToken)
)
{
var ips = await GetBlockedIndexAsync(cancellationToken);
if (ips.Count == 0)
{
return [];
}
var now = DateTime.UtcNow;
var blocked = new List<BlockedIpInfo>();
var activeIps = new List<string>();
foreach (var ip in ips)
{
var record = await GetRecordForUpdateAsync(ip, cancellationToken);
if (record is { BlockedUntil: { } blockedUntil } && blockedUntil > now)
{
blocked.Add(
new BlockedIpInfo
{
Ip = ip,
BlockedUntil = blockedUntil,
EventCount = record.EventCount,
}
);
activeIps.Add(ip);
}
}
// 重写索引,惰性清理已过期的 IP
if (activeIps.Count != ips.Count)
{
await SetBlockedIndexAsync(activeIps, cancellationToken);
}
return blocked;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
logger.LogWarning(ex, "查询封禁 IP 列表失败");
return [];
}
}
/// <summary>
/// 将 IP 从封禁索引移除(幂等)
/// </summary>
private async Task RemoveFromBlockedIndexAsync(string ip, CancellationToken cancellationToken)
{
await using (
await distributedLockProvider
.CreateLock(BlockedIndexLockKey)
.AcquireAsync(cancellationToken: cancellationToken)
)
{
var ips = await GetBlockedIndexAsync(cancellationToken);
if (!ips.Remove(ip))
{
return;
}
await SetBlockedIndexAsync(ips, cancellationToken);
}
}
/// <summary>
/// 将 IP 加入封禁索引(幂等)
/// </summary>
private async Task AddToBlockedIndexAsync(string ip, CancellationToken cancellationToken)
{
try
{
await using (
await distributedLockProvider
.CreateLock(BlockedIndexLockKey)
.AcquireAsync(cancellationToken: cancellationToken)
)
{
var ips = await GetBlockedIndexAsync(cancellationToken);
if (ips.Contains(ip))
{
return;
}
ips.Add(ip);
await SetBlockedIndexAsync(ips, cancellationToken);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
// 索引写入失败不影响封禁本身
logger.LogWarning(ex, "写入封禁索引失败,IP: {Ip}", ip);
}
}
/// <summary>
/// 读取封禁索引,锁内必须跳过本地 L1 副本避免读到旧值
/// </summary>
private async Task<List<string>> GetBlockedIndexAsync(CancellationToken cancellationToken)
{
var result = await fusionCache.TryGetAsync<List<string>>(
BlockedIndexKey,
options =>
{
options.SkipMemoryCacheRead = true;
options.AllowBackgroundDistributedCacheOperations = false;
options.IsFailSafeEnabled = false;
options.ReThrowDistributedCacheExceptions = true;
},
token: cancellationToken
);
return result.HasValue ? result.Value : [];
}
private async Task SetBlockedIndexAsync(List<string> ips, CancellationToken cancellationToken)
{
await fusionCache.SetAsync(
BlockedIndexKey,
ips,
options => options.SetDuration(BlockedIndexDuration),
token: cancellationToken
);
}
private async Task<IpAccessRecord?> GetRecordAsync(
string ip,
CancellationToken cancellationToken
)
{
var result = await fusionCache.TryGetAsync<IpAccessRecord>(
GetCacheKey(ip),
token: cancellationToken
);
return result.HasValue ? result.Value : null;
}
private async Task<IpAccessRecord?> GetRecordForUpdateAsync(
string ip,
CancellationToken cancellationToken
)
{
var result = await fusionCache.TryGetAsync<IpAccessRecord>(
GetCacheKey(ip),
options =>
{
// 锁内必须读取共享缓存,不能使用可能过期的本地 L1 副本
options.SkipMemoryCacheRead = true;
options.AllowBackgroundDistributedCacheOperations = false;
options.IsFailSafeEnabled = false;
options.ReThrowDistributedCacheExceptions = true;
},
token: cancellationToken
);
return result.HasValue ? result.Value : null;
}
private async Task SetRecordAsync(
string ip,
IpAccessRecord record,
int durationMinutes,
CancellationToken cancellationToken
)
{
await fusionCache.SetAsync(
GetCacheKey(ip),
record,
options =>
{
// 过期时间覆盖窗口和封禁期,到期自动清理
options.SetDuration(TimeSpan.FromMinutes(durationMinutes));
// 释放分布式锁前必须完成共享缓存写入,避免下一节点读到旧值
options.AllowBackgroundDistributedCacheOperations = false;
options.IsFailSafeEnabled = false;
options.ReThrowDistributedCacheExceptions = true;
},
token: cancellationToken
);
}
private static string GetCacheKey(string ip) => $"{CacheKeyPrefix}{ip.Replace(':', '-')}";
private static string GetLockKey(string ip)
{
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(ip)));
return $"RateLimit:Lock:{hash}";
}
private static IpAccessRecord CreateRecord() =>
new()
{
WindowStart = DateTime.UtcNow,
LastEventTime = DateTime.UtcNow,
EventCount = 0,
};
private static void ResetWindow(IpAccessRecord record, DateTime now)
{
record.WindowStart = now;
record.EventCount = 0;
// 保留 BlockedUntil,封禁期内的窗口重置不影响封禁状态
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释:IP 限流服务实现
这是一个基于滑动时间窗口的 IP 限流服务实现,用于检测和阻止可疑的 IP 访问行为。
核心功能
1. 限流机制
- 时间窗口计数:在配置的时间窗口内(
WindowMinutes)统计 IP 的可疑事件次数 - 阈值封禁:事件数达到阈值(
MaxEvents)后,封禁该 IP 一段时间(BlockMinutes) - 自动解封:封禁期过后自动解除限制
2. 主要方法
CheckLimitStatusAsync(ip)
检查 IP 是否被封禁:
// 返回三种状态:
// - 允许访问(未封禁或封禁已过期)
// - 被封禁(返回剩余时间和延迟响应时间)
// - 降级放行(缓存故障时保证服务可用性)
RecordEventAsync(ip, reason)
记录可疑事件:
// 1. 获取分布式锁(防止并发问题)
// 2. 读取当前窗口记录
// 3. 判断窗口是否过期,过期则重置
// 4. 增加事件计数
// 5. 达到阈值则封禁 IP 并加入索引
BlockIpAsync(ip, minutes) / UnblockIpAsync(ip)
手动封禁/解封 IP:
// 管理员可以手动干预,立即封禁或解除封禁
GetBlockedIpsAsync()
查询封禁列表:
// 返回当前被封禁的 IP 列表
// 同时惰性清理已过期的 IP
技术亮点
1. 分布式锁
await distributedLockProvider.CreateLock(GetLockKey(ip)).AcquireAsync()
- 使用
Medallion.Threading确保分布式环境下的数据一致性 - 每个 IP 使用独立的锁(锁粒度优化)
2. 两层缓存策略(FusionCache)
- L1(内存缓存):快速读取
- L2(分布式缓存):跨节点共享数据
- 关键操作跳过 L1:
options.SkipMemoryCacheRead = true; // 锁内必须读取最新数据 options.AllowBackgroundDistributedCacheOperations = false; // 同步写入
3. 封禁索引管理
BlockedIndexKey // 维护当前封禁 IP 列表
- 便于快速查询所有封禁 IP
- 惰性清理过期记录(在查询时自动清理)
4. 容错设计
catch (Exception ex) {
logger.LogWarning(ex, "检查 IP {Ip} 限流状态失败,降级放行", ip);
return RateLimitResult.Allow(); // 降级策略:宁可放行也不误拦
}
5. 安全细节
- 锁 Key 使用 SHA256 哈希(避免特殊字符)
- IP 地址中的
:替换为-(兼容 Redis 等缓存系统) - 支持取消令牌(CancellationToken)
数据结构
IpAccessRecord(存储结构,代码未给出但可推断)
class IpAccessRecord {
DateTime WindowStart; // 当前窗口开始时间
DateTime LastEventTime; // 最后事件时间
int EventCount; // 窗口内事件计数
DateTime? BlockedUntil; // 封禁截止时间
}
BlockedIpInfo(返回结构)
class BlockedIpInfo {
string Ip;
DateTime BlockedUntil;
int EventCount;
}
使用场景
- 登录失败限流:多次登录失败封禁 IP
- API 频率控制:防止恶意调用
- 爬虫防护:检测异常访问模式
- DDoS 防护:快速封禁攻击源
配置示例(推断)
RateLimitConfig {
WindowMinutes = 5; // 5分钟窗口
MaxEvents = 10; // 最多10次事件
BlockMinutes = 30; // 封禁30分钟
BlockDelaySeconds = 3; // 封禁期间请求延迟3秒响应
}
这是一个生产级的限流实现,考虑了分布式、高并发、容错等多方面因素。
AI 正在分析代码…
评论加载中...