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;
}

使用场景

  1. 登录失败限流:多次登录失败封禁 IP
  2. API 频率控制:防止恶意调用
  3. 爬虫防护:检测异常访问模式
  4. DDoS 防护:快速封禁攻击源

配置示例(推断)

RateLimitConfig {
    WindowMinutes = 5;        // 5分钟窗口
    MaxEvents = 10;           // 最多10次事件
    BlockMinutes = 30;        // 封禁30分钟
    BlockDelaySeconds = 3;    // 封禁期间请求延迟3秒响应
}

这是一个生产级的限流实现,考虑了分布式、高并发、容错等多方面因素。

评论加载中...