基本IP保护 Swagger UI 的中间件

实现说明

  1. 配置文件(appsettings.json)

    • 集中管理 Swagger 相关配置,包括允许访问的 IP 列表、文档标题和版本
    • 支持环境特定配置(如 appsettings.Development.json),方便多环境部署
  2. SwaggerSettings 类

    • 强类型绑定配置文件中的 Swagger 节点
    • 提供类型安全的配置访问,避免硬编码字符串键
  3. SwaggerIPFilterMiddleware 中间件

    • 自动拦截 Swagger 相关路径(/swagger 和 /swagger-ui)
    • 支持获取真实客户端 IP(包括反向代理场景)
    • 处理 IP 格式清洗(移除端口号,兼容 IPv4 和 IPv6)
    • 结合日志记录未授权访问尝试
  4. Program.cs 配置

    • 启用配置文件热重载(reloadOnChange: true)
    • 通过 IOptions 模式注入配置,自动感知配置变化
    • 提供扩展方法简化中间件注册

使用方法

  1. 修改 appsettings.json 中的 Swagger:AllowedIPs 添加允许访问的 IP
  2. 无需重启应用,配置会自动生效
  3. 非允许 IP 访问 Swagger 时会收到 403 禁止访问响应。

1、Program.cs

cs 复制代码
using SwaggerIpFilterDemo;

var builder = WebApplication.CreateBuilder(args);

// 配置:启用配置文件热重载
builder.Configuration
    .SetBasePath(builder.Environment.ContentRootPath)
    .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", reloadOnChange: true, optional: true)
    .AddEnvironmentVariables();

// 注册Swagger配置
builder.Services.Configure<SwaggerSettings>(builder.Configuration.GetSection("Swagger"));

// 添加控制器服务
builder.Services.AddControllers();

// 添加Swagger服务
builder.Services.AddSwaggerGen(c =>
{
    var swaggerSettings = builder.Configuration.GetSection("Swagger").Get<SwaggerSettings>();
    c.SwaggerDoc(swaggerSettings.Version, new() { Title = swaggerSettings.Title, Version = swaggerSettings.Version });
});

var app = builder.Build();

// 开发环境启用详细异常页
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

// 注册Swagger IP过滤中间件(必须在UseSwagger之前)
app.UseSwaggerIPFilter();

// 启用Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    var swaggerSettings = app.Services.GetRequiredService<IOptions<SwaggerSettings>>().Value;
    c.SwaggerEndpoint($"/swagger/{swaggerSettings.Version}/swagger.json", swaggerSettings.Title);
});

// 其他中间件
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();

app.MapControllers();

app.Run();

2、SwaggerIPFilterMiddleware.cs

cs 复制代码
namespace SwaggerIpFilterDemo;

/// <summary>
/// Swagger IP访问过滤中间件
/// </summary>
public class SwaggerIPFilterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IOptions<SwaggerSettings> _swaggerSettings;
    private readonly ILogger<SwaggerIPFilterMiddleware> _logger;

    /// <summary>
    /// 构造函数
    /// </summary>
    public SwaggerIPFilterMiddleware(
        RequestDelegate next,
        IOptions<SwaggerSettings> swaggerSettings,
        ILogger<SwaggerIPFilterMiddleware> logger)
    {
        _next = next;
        _swaggerSettings = swaggerSettings;
        _logger = logger;
    }

    /// <summary>
    /// 中间件执行逻辑
    /// </summary>
    public async Task InvokeAsync(HttpContext context)
    {
        // 只拦截Swagger相关请求
        if (context.Request.Path.StartsWithSegments("/swagger") || 
            context.Request.Path.StartsWithSegments("/swagger-ui"))
        {
            // 获取客户端真实IP(支持反向代理场景)
            var clientIp = GetClientIpAddress(context);
            
            // 处理IP格式(移除端口号)
            var cleanIp = CleanIpAddress(clientIp);
            
            // 检查是否为允许的IP
            var allowedIps = _swaggerSettings.Value.AllowedIPs;
            var isAllowed = allowedIps.Contains(cleanIp) || 
                           cleanIp == "127.0.0.1" || 
                           cleanIp == "::1";

            if (!isAllowed)
            {
                _logger.LogWarning("Swagger访问被拒绝,IP: {Ip}", cleanIp);
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
                await context.Response.WriteAsync($"禁止访问Swagger文档,您的IP: {cleanIp}");
                return;
            }
        }

        // 允许访问,继续处理后续中间件
        await _next(context);
    }

    /// <summary>
    /// 获取客户端真实IP(支持反向代理)
    /// </summary>
    private string GetClientIpAddress(HttpContext context)
    {
        // 优先从反向代理头获取(如Nginx/Apache设置的X-Forwarded-For)
        var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        if (!string.IsNullOrEmpty(forwardedFor))
        {
            return forwardedFor.Split(',', StringSplitOptions.TrimEntries).First();
        }

        // 直接从连接获取IP
        return context.Connection.RemoteIpAddress?.ToString() ?? "未知IP";
    }

    /// <summary>
    /// 清理IP地址(移除端口号)
    /// </summary>
    private string CleanIpAddress(string ip)
    {
        if (string.IsNullOrEmpty(ip)) return ip;
        
        // 处理带端口的IP(如 "192.168.1.100:5000" → "192.168.1.100")
        if (ip.Contains(':'))
        {
            // 区分IPv6(如"fe80::1:2:3:4:5")和带端口的IPv4
            var parts = ip.Split(':');
            return parts.Length > 2 ? ip : parts[0];
        }

        return ip;
    }
}

/// <summary>
/// 中间件扩展方法,简化注册
/// </summary>
public static class SwaggerIPFilterMiddlewareExtensions
{
    public static IApplicationBuilder UseSwaggerIPFilter(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SwaggerIPFilterMiddleware>();
    }
}

3、SwaggerSettings.cs

cs 复制代码
namespace SwaggerIpFilterDemo;

/// <summary>
/// Swagger配置选项
/// </summary>
public class SwaggerSettings
{
    /// <summary>
    /// 允许访问Swagger的IP列表
    /// </summary>
    public List<string> AllowedIPs { get; set; } = new List<string>();
    
    /// <summary>
    /// 文档标题
    /// </summary>
    public string Title { get; set; } = "API文档";
    
    /// <summary>
    /// 文档版本
    /// </summary>
    public string Version { get; set; } = "v1";
}

4、appsettings.json

cs 复制代码
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Swagger": {
    "AllowedIPs": [
      "127.0.0.1",    // 本地IPv4
      "::1",          // 本地IPv6
      "192.168.1.100",
      "192.168.1.101"
    ],
    "Title": "API文档",
    "Version": "v1"
  }
}
相关推荐
阿昌喜欢吃黄桃3 天前
RocketMq事务消息原理
java·中间件·消息队列·rocketmq·mq
半夜修仙4 天前
延迟队列的介绍及常见问题
java·数据库·中间件·rabbitmq
手握风云-4 天前
一条消息的旅程:RabbitMQ 学习与实践(一)
中间件·rabbitmq
RH2312115 天前
2026.6.8Linux
java·数据库·中间件
理人综艺好会5 天前
双Token机制在实际项目中的应用与实践
中间件·token
番茄去哪了6 天前
神领物流面试题(一)
java·大数据·中间件
念何架构之路6 天前
消息中间件
中间件
都说名字长不会被发现6 天前
Spring Boot Starter 中间件账号密码加密方案设计与实现
java·spring boot·后端·中间件
瀚高PG实验室7 天前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库
之歆7 天前
Day11_Express 深入解析:从中间件到项目实战
中间件·express