using Dpz.Core.Infrastructure.Intercept;
using Dpz.Core.Infrastructure.RateLimiting;
using Dpz.Core.Public.Entity;
using Microsoft.Extensions.Primitives;
using Wangkanai.Detection.Models;
using Wangkanai.Detection.Services;

namespace Dpz.Core.Web.Library.Middleware;

public class RejectBotsMiddleware(
    RequestDelegate next,
    IConfiguration configuration,
    IWebHostEnvironment webHostEnvironment,
    ILogger<RejectBotsMiddleware> logger,
    IIpRateLimitService rateLimitService,
    IInterceptRuleService interceptRuleService
)
{
    public async Task Invoke(
        HttpContext httpContext,
        IBlockService blockService,
        IDetectionService detectionService
    )
    {
        // 0. 已登录用户直接忽略
        if (httpContext.User.Authenticated
#if DEBUG
            || webHostEnvironment.IsDevelopment()
#endif
        )
        {
            await next.Invoke(httpContext);
            return;
        }

        var requestPath = httpContext.Request.Path.Value ?? "";
        var ignoreRequestPrefixes =
            configuration.GetSection("IgnoreRequestPrefix").Get<List<string>>() ?? [];

        // 1. 忽略列表优先处理(IgnoreRequestPrefix)
        // 如果当前规则在忽略列表内,则彻底忽略,不进行下一步
        if (
            ignoreRequestPrefixes.Any(x =>
                requestPath.StartsWith(x, StringComparison.OrdinalIgnoreCase)
            )
        )
        {
            await next.Invoke(httpContext);
            return;
        }

        var clientIp = httpContext.Request.GetIpAddress();

        // 2. 检查自定义拦截规则(规则 > 黑名单)
        // 如果命中规则,下一步,并且记录
        var rule = interceptRuleService.CheckInterceptRules(httpContext);
        if (rule != null)
        {
            logger.LogInformation(
                "命中规则拦截,RequestPath: {RequestPath},Rule:{@Rule}",
                httpContext.Request.Path,
                rule
            );
            // 记录IP拦截
            await rateLimitService.RecordBlockAsync(clientIp, "命中拦截规则");
            await SaveBlockAndResponse403Async(blockService, httpContext);
            return;
        }

        // 拦截空 User-Agent 请求 (也可视为规则的一种)
        if (StringValues.IsNullOrEmpty(httpContext.Request.Headers.UserAgent))
        {
            logger.LogInformation(
                "空User-Agent拦截,RequestPath:{RequestPath}",
                httpContext.Request.Path
            );
            // 记录IP拦截
            await rateLimitService.RecordBlockAsync(clientIp, "空User-Agent");
            await SaveBlockAndResponse403Async(blockService, httpContext);
            return;
        }

        // 拦截特定浏览器 (也可视为规则的一种)
        if (
            (
                detectionService.Browser.Name is Browser.Chrome or Browser.Firefox or Browser.Edge
                && detectionService.Browser.Version < new Version("100.0.0.0")
            )
            || detectionService.Browser.Name is Browser.InternetExplorer
        )
        {
            logger.LogInformation(
                "浏览器拦截,浏览器:{Browser},版本:{Version}",
                detectionService.Browser.Name,
                detectionService.Browser.Version
            );
            // 记录IP拦截
            await rateLimitService.RecordBlockAsync(clientIp, "低版本浏览器");
            await Response403Async(httpContext);
            return;
        }

        // 3. 检查是否匹配黑名单
        // 获取黑名单数据 (延迟获取,只有通过前面规则检查的才需要查询黑名单)
        List<VmBlock> blocks = [];
        try
        {
            blocks = await blockService.GetBlocksAsync(30);
        }
        catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested)
        {
            return;
        }
        catch (Exception e)
        {
            logger.LogError(e, "获取黑名单失败");
        }

        // 如果命中黑名单,下一步,并且记录
        if (await CheckBlockListAsync(blockService, httpContext, blocks))
        {
            logger.LogInformation(
                "黑名单拦截,RequestMethod:{RequestMethod}, RequestPath:{RequestPath}",
                httpContext.Request.Method,
                httpContext.Request.Path
            );
            // 记录IP拦截
            await rateLimitService.RecordBlockAsync(clientIp, "黑名单");
            await SaveBlockAndResponse403Async(blockService, httpContext);
            return;
        }

        await next.Invoke(httpContext);

        // 4. 记录404请求
        // 如果404,如果在忽略列表内,忽略下一步,不在忽略列表内,记录
        // (注:由于前面已经对忽略列表进行了检查并return,所以理论上这里只处理非忽略列表的请求)
        await Record404NotFoundAsync(blockService, httpContext, requestPath, ignoreRequestPrefixes);
    }

    /// <summary>
    /// 检查是否匹配黑名单
    /// </summary>
    private async Task<bool> CheckBlockListAsync(
        IBlockService blockService,
        HttpContext httpContext,
        List<VmBlock> blocks
    )
    {
        var requestPath = httpContext.Request.Path.Value ?? "";
        if (string.IsNullOrWhiteSpace(requestPath))
        {
            return false;
        }

        // 查找匹配的黑名单项(不区分大小写)
        var matchedBlock = blocks.FirstOrDefault(x =>
            string.Equals(x.RequestPath, requestPath, StringComparison.OrdinalIgnoreCase)
        );

        if (matchedBlock != null)
        {
            // 增加访问计数
            await blockService.IncrementCountAsync(
                matchedBlock.RequestMethod,
                matchedBlock.RequestPath
            );
            logger.LogInformation("黑名单拦截: {RequestPath}", requestPath);
            return true;
        }

        return false;
    }

    /// <summary>
    /// 保存黑名单并返回403响应
    /// </summary>
    private async Task SaveBlockAndResponse403Async(
        IBlockService blockService,
        HttpContext httpContext
    )
    {
        var requestPath = httpContext.Request.Path.Value ?? "";
        if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/")
        {
            return;
        }

        var ipAddress = httpContext
            .Request.GetIpAddress()
            .Split(',')
            .Where(x => !string.IsNullOrEmpty(x))
            .Select(x => x.Trim())
            .ToArray();

        var userAgent = httpContext.Request.Headers.UserAgent.ToString();
        var method = httpContext.Request.Method.ToUpper();

        var block = new Block
        {
            RequestMethod = method,
            RequestPath = requestPath,
            IpAddresses = ipAddress,
            UserAgents = [userAgent],
        };

        // 先保存黑名单记录
        await blockService.SaveBlockAsync(block);

        // 再尝试返回403响应(内部会检查响应状态)
        await Response403Async(httpContext);
    }

    /// <summary>
    /// 记录404请求
    /// </summary>
    private async Task Record404NotFoundAsync(
        IBlockService blockService,
        HttpContext httpContext,
        string requestPath,
        List<string> ignoreRequestPrefixes
    )
    {
        if (httpContext.Response.StatusCode != 404)
        {
            return;
        }
        if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/")
        {
            return;
        }

        if (IsUuidNoiseRequest(httpContext, requestPath))
        {
            return;
        }

        // 检查是否在忽略列表中
        if (
            ignoreRequestPrefixes.Any(x =>
                requestPath.StartsWith(x, StringComparison.OrdinalIgnoreCase)
            )
        )
        {
            return;
        }

        var ipAddress = httpContext
            .Request.GetIpAddress()
            .Split(',')
            .Where(x => !string.IsNullOrEmpty(x))
            .Select(x => x.Trim())
            .ToArray();

        var userAgent = httpContext.Request.Headers.UserAgent.ToString();
        var method = httpContext.Request.Method.ToUpper();

        var block = new Block
        {
            RequestMethod = method,
            RequestPath = requestPath,
            IpAddresses = ipAddress,
            UserAgents = [userAgent],
        };

        await blockService.SaveBlockAsync(block);
    }

    /// <summary>
    /// 过滤前端 blob/hls 等场景产生的 /{UUID} 噪声请求,避免污染黑名单。
    /// </summary>
    private static bool IsUuidNoiseRequest(HttpContext httpContext, string requestPath)
    {
        if (
            !HttpMethods.IsGet(httpContext.Request.Method)
            && !HttpMethods.IsHead(httpContext.Request.Method)
        )
        {
            return false;
        }

        var segment = requestPath.Trim('/');
        if (string.IsNullOrWhiteSpace(segment) || segment.Contains('/'))
        {
            return false;
        }

        return Guid.TryParseExact(segment, "D", out _)
            || Guid.TryParseExact(segment, "N", out _)
            || ObjectId.TryParse(segment, out _);
    }

    /// <summary>
    /// 返回403响应
    /// </summary>
    private async Task Response403Async(HttpContext httpContext)
    {
        // 检查响应是否已经开始,避免 "response has already started" 错误
        if (httpContext.Response.HasStarted)
        {
            logger.LogWarning(
                "尝试返回403响应时发现响应已开始,IP: {ClientIp}, 路径: {RequestPath}",
                httpContext.Request.GetIpAddress(),
                httpContext.Request.Path
            );
            return;
        }

        httpContext.Response.StatusCode = 403;
        httpContext.Response.ContentType = "text/html";
        await httpContext.Response.SendFileAsync(
            Path.Combine(webHostEnvironment.WebRootPath, "reject.html")
        );
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个ASP.NET Core中间件,名为RejectBotsMiddleware,主要用于识别和拦截恶意请求、机器人访问以及维护黑名单系统。让我详细解释其功能和工作流程:

主要功能

1. 多层防护机制

  • 自定义拦截规则检查
  • 浏览器版本检查
  • 黑名单匹配
  • 404请求记录

2. 智能过滤策略

详细工作流程

第0步:用户身份检查

if (httpContext.User.Authenticated || webHostEnvironment.IsDevelopment())
{
    await next.Invoke(httpContext);
    return;
}
  • 已登录用户直接放行
  • 开发环境下直接放行

第1步:忽略列表检查

if (ignoreRequestPrefixes.Any(x => 
    requestPath.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
{
    await next.Invoke(httpContext);
    return;
}
  • 检查请求路径是否在配置的忽略列表中
  • 如果在忽略列表中,直接放行

第2步:自定义拦截规则检查

  • 检查是否命中自定义拦截规则
  • 拦截空User-Agent请求
  • 拦截低版本浏览器(Chrome/Firefox/Edge < 100.0.0.0,IE浏览器)

第3步:黑名单检查

var matchedBlock = blocks.FirstOrDefault(x =>
    string.Equals(x.RequestPath, requestPath, StringComparison.OrdinalIgnoreCase));
  • 延迟加载黑名单数据(只有通过前面检查的请求才查询)
  • 匹配黑名单中的请求路径

第4步:404请求记录

  • 记录404状态的请求到黑名单
  • 过滤UUID噪声请求(如前端blob/hls产生的随机请求)

核心辅助方法

SaveBlockAndResponse403Async

  • 保存恶意请求信息到黑名单
  • 返回403 Forbidden响应

IsUuidNoiseRequest

return Guid.TryParseExact(segment, "D", out _)
    || Guid.TryParseExact(segment, "N", out _)
    || ObjectId.TryParse(segment, out _);
  • 智能识别前端产生的UUID格式噪声请求
  • 避免这些请求污染黑名单

Response403Async

  • 安全地返回403响应
  • 检查响应是否已开始,避免异常
  • 返回自定义的reject.html页面

设计亮点

  1. 分层防护:从用户认证 → 忽略列表 → 规则检查 → 黑名单 → 404记录
  2. 性能优化:延迟加载黑名单,只对需要的请求进行数据库查询
  3. 智能过滤:自动识别并过滤前端产生的噪声请求
  4. 详细日志:每个拦截环节都有相应的日志记录
  5. 安全响应:检查响应状态,避免重复设置响应头

这个中间件是一个完整的Web应用安全防护解决方案,能够有效防止恶意访问、机器人攻击,并维护一个动态的黑名单系统。

评论加载中...