using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
using AgileConfig.Client;
using Dpz.Core.Hangfire;
using Dpz.Core.Infrastructure.Configuration;
using Dpz.Core.WebApi.Security;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.OpenApi;
using Scalar.AspNetCore;

const string originsName = "Open";
Log.Logger = new LoggerConfiguration().Enrich.FromLogContext().CreateBootstrapLogger();
try
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseAgileConfig(new ConfigClient(builder.Configuration));

    var configuration = builder.Configuration;

    var seq = configuration.GetSection("LogSeq").Get<LogSeq>();
    builder.Host.ConfigurationLog(seq);

    #region services

    var services = builder.Services;

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        options.KnownIPNetworks.Clear();
        options.KnownProxies.Clear();
    });

    services.AddRateLimiter(options =>
    {
        options.AddFixedWindowLimiter(
            "comment",
            opt =>
            {
                opt.AutoReplenishment = true;
                opt.PermitLimit = 3;
                opt.Window = TimeSpan.FromMinutes(1);
                opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
                opt.QueueLimit = 2;
            }
        );
    });

    // IoC
    services.AddDefaultServices(configuration).AddAppScoped();

    services
        .AddControllers()
        // .AddXmlDataContractSerializerFormatters()
        //.AddXmlSerializerFormatters()
        .AddMessagePackSerializerFormatters()
        .AddJsonOptions(opts =>
        {
            opts.JsonSerializerOptions.Converters.Add(new TimeSpanConverter());
            opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        });

    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat([
            "application/font-woff2",
            "image/svg+xml",
            "text/plain",
            "application/lrc",
        ]);
    });

    services.Configure<BrotliCompressionProviderOptions>(options =>
    {
        options.Level = CompressionLevel.Optimal;
    });

    services.Configure<GzipCompressionProviderOptions>(options =>
    {
        options.Level = CompressionLevel.Optimal;
    });

    services.Configure<KestrelServerOptions>(options =>
    {
        options.Limits.MaxRequestBodySize = int.MaxValue;
    });

    services.AddEndpointsApiExplorer();
    services.AddOutputCache(options =>
    {
        options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
    });
    services.AddOpenApi(options =>
    {
        options.AddDocumentTransformer(
            async (doc, _, cancelToken) =>
            {
                var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
                var description = "";
                if (!string.IsNullOrEmpty(basePath))
                {
                    var readmePath = Path.Combine(basePath, "README.md");
                    if (File.Exists(readmePath))
                    {
                        description = await File.ReadAllTextAsync(readmePath, cancelToken);
                    }
                }

                doc.Info.Description = description;

                doc.Components ??= new OpenApiComponents();
                doc.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
                doc.Components.SecuritySchemes.TryAdd(
                    "Bearer",
                    new OpenApiSecurityScheme
                    {
                        Description = "使用Bearer方案的JWT授权标头(示例:“ Bearer JWT_Token”)",
                        Name = "Authorization",
                        In = ParameterLocation.Header,
                        Type = SecuritySchemeType.OAuth2,
                        Scheme = "Bearer",
                        BearerFormat = "JWT",
                    }
                );

                doc.Security ??= new List<OpenApiSecurityRequirement>();
                doc.Security.Add(
                    new OpenApiSecurityRequirement
                    {
                        {
                            new OpenApiSecuritySchemeReference("Bearer")
                            {
                                Reference = new OpenApiReferenceWithDescription
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "Bearer",
                                },
                            },
                            []
                        },
                    }
                );
            }
        );
    });

    services.AddBusinessServices(configuration);

    // Cors
    services.AddCors(options =>
    {
        options.AddPolicy(
            originsName,
            cfg =>
            {
                var origins = configuration.GetSection("Origins").Get<string[]>() ?? [];
                cfg.WithOrigins(origins)
                    .WithMethods("GET", "PUT", "POST", "DELETE", "PATCH")
                    .AllowAnyHeader();
            }
        );
    });

    var issuer = configuration["Server:Issuer"] ?? throw new InvalidConfigurationException();
    services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = issuer;
            options.Audience = "resource_server";
            options.RequireHttpsMetadata = true;
            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    Log.Error(context.Exception, "Failed to authenticate");
                    return Task.CompletedTask;
                },
            };
        });

    services.AddPermissionAuthorization();

    services.AddRazorPages();

    var webApiHangfireCollectionPrefix =
        configuration["WebApiHangfireCollectionPrefix"]
        ?? throw new InvalidConfigurationException();
    services.AddHangfireService(configuration, webApiHangfireCollectionPrefix);

    #endregion

    var app = builder.Build();

    #region configuration

    if (app.Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseForwardedHeaders();

    app.UseSerilogRequestLogging(options =>
    {
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
            diagnosticContext.Set(
                "UserAgent",
                httpContext.Request.Headers["User-Agent"].ToString()
            );
            diagnosticContext.Set("IpAddress", httpContext.Request.GetIpAddress());
        };
    });

    app.UseResponseHeaders().UseRequestRecord().UseDateTimeLocalFormat();

    app.UseResponseCompression();

    app.MapOpenApi().CacheOutput();
    app.MapScalarApiReference(options =>
    {
        options.Title = "Dpz.Core.WebApi";
        options.Favicon = "https://dpangzi.com/favicon.ico";
        options.Theme = ScalarTheme.Moon;
        options.HideDarkModeToggle = false;
    });

    app.GetHostingEnvironment();

    // //强制HTTPS
    // app.UseHttpsRedirection();

    app.UseRouting();

    app.MapRazorPages();

    app.UseCors(originsName);

    //身份认证
    app.UseAuthentication();
    //身份授权
    app.UseAuthorization();

    app.MapControllers();
    //app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

#if DEBUG
    app.HangfireSetting();
#endif

    #endregion

    app.MapGet("/ping", () => "pong");
    app.MapGet(
        "/datetime",
        () => $"{DateTime.Now:yyyy/Mm/dd HH:mm:ss}\r\n{Environment.MachineName}"
    );
    Log.Information("Starting web api host,current process id:{ID}", Environment.ProcessId);
    app.Run();
}
catch (Exception e)
{
    Console.Error.WriteLine(e);
    Log.Fatal(e, "Host terminated unexpectedly");
}

/// <summary>
///
/// </summary>
public partial class Program
{
    /// <summary>
    /// 身份认证Cookie名称
    /// </summary>
    public const string AuthorizeCookieName = "Dpz.Web.Api.Server.Authoriza";

    /// <summary>
    /// 账号Cookie名称
    /// </summary>
    public const string AccountCookieName = "Dpz.Web.Api.Server.Account";
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

下面把这段 ASP.NET Core 程序的主要功能与关键点分块解释,先给出总体概览,再按服务注册、请求中间件管道、公开的端点等细节说明,并指出几个可能的注意事项。

总体概览

  • 该程序是一个 Web API 主机的启动代码(Program.cs),使用最小宿主/最小 Api 风格的启动方式(WebApplication)。
  • 集成了配置中心(AgileConfig)、结构化日志(Serilog/Seq)、OpenAPI 文档、自定义 UI(Scalar)、速率限制、响应压缩、输出缓存、权限/认证(JWT)、Hangfire(后台任务)等常见功能。
  • 对外暴露了控制器、Razor Page、OpenAPI 界面以及简单的 /ping、/datetime 测试端点。

主要步骤与功能细分

  1. 启动与配置读取
  • 创建 WebApplicationBuilder 并通过 builder.Host.UseAgileConfig(new ConfigClient(builder.Configuration)) 集成 AgileConfig 配置客户端。
  • 从配置加载 LogSeq(用于日志发送到 Seq)并通过扩展方法 builder.Host.ConfigurationLog(seq) 配置日志(代码里用 Serilog 的引导器创建了基础 logger)。
  1. 服务注册(services)
  • Forwarded headers:
    • 配置 ForwardedHeadersOptions 接受 X-Forwarded-For 和 X-Forwarded-Proto,并清空 KnownIPNetworks 和 KnownProxies(表示不会针对代理 IP 做白名单限制,见注意事项)。
  • Rate limiting:
    • 使用 ASP.NET Core 的 RateLimiting 中间件,添加了一个名为 "comment" 的 FixedWindowLimiter:每 1 分钟最多 permitLimit = 3 个请求,自动补充,队列上限 queueLimit = 2,队列处理顺序 OldestFirst(超出请求将被拒绝/排队)。
  • IoC/业务服务:
    • 通过 AddDefaultServices(configuration).AddAppScoped()、AddBusinessServices(configuration) 等扩展方法注册应用业务相关服务(这些是项目自定义扩展)。
  • 控制器与序列化:
    • 添加 Controllers,并支持 MessagePack 序列化格式(AddMessagePackSerializerFormatters)。
    • JSON 选项里添加了两个转换器:TimeSpanConverter(自定义)和 JsonStringEnumConverter(枚举以字符串序列化)。
  • 响应压缩:
    • 添加 Brotli 与 Gzip 提供者,压缩级别设为 Optimal。
    • 增加了额外 MIME 类型(如 font-woff2、image/svg+xml、text/plain、application/lrc 等)以启用压缩。
  • Kestrel:
    • 将 MaxRequestBodySize 设为 int.MaxValue(允许非常大的请求体)。
  • 输出缓存(OutputCache):
    • 添加了基础策略:默认过期时间 10 分钟。
  • OpenAPI(Swagger / OpenAPI 文档):
    • 使用 AddOpenApi 并添加 DocumentTransformer:从程序目录读取 README.md 作为文档描述,向文档添加 Security Scheme(“Bearer”)并在全局安全要求中引入。
    • 注意:代码中将 SecuritySchemeType 设为 OAuth2,但同时使用 Bearer、Scheme 等字段,实际应根据需求确认(见注意事项)。
  • CORS:
    • 添加名为 originsName ("Open") 的策略,从配置中的 "Origins" 节点获取允许的源,并允许 GET/PUT/POST/DELETE/PATCH 与任意头。
  • 身份认证(JWT):
    • 添加 JwtBearer 认证,Authority(授权中心)从配置 Server:Issuer 获取,Audience 设为 "resource_server",RequireHttpsMetadata = true。
    • 在 OnAuthenticationFailed 事件中写日志。
  • 授权:
    • 注册自定义的权限授权(services.AddPermissionAuthorization())。
  • Razor Pages 与 Hangfire:
    • 注册 Razor Pages。
    • 通过 services.AddHangfireService(configuration, webApiHangfireCollectionPrefix) 集成 Hangfire(redis/mongo 等持久化集合前缀由配置提供)。
  1. 构建应用并配置中间件管道(app)
  • 开发时显示 DeveloperExceptionPage。
  • UseForwardedHeaders:使应用读取转发头(X-Forwarded-*)。
  • Serilog 请求日志:调用 UseSerilogRequestLogging,并在 EnrichDiagnosticContext 中写入 Host、Scheme、UserAgent、IpAddress 等信息。
  • 自定义中间件:
    • UseResponseHeaders、UseRequestRecord、UseDateTimeLocalFormat:项目自定义的中间件,分别用于响应头、请求记录、日期时间格式化等。
  • 响应压缩中间件:UseResponseCompression。
  • OpenAPI 与 Scalar UI:
    • app.MapOpenApi().CacheOutput();(对 OpenAPI 文档做输出缓存)
    • app.MapScalarApiReference(...):映射自定义的 API 文档/界面(设置标题、favicon、主题等)。
  • 路由与静态页面:
    • app.UseRouting(); app.MapRazorPages();
  • CORS 生效:app.UseCors(originsName);
  • 认证与授权:
    • app.UseAuthentication(); app.UseAuthorization();
  • 控制器映射:app.MapControllers();
  • Hangfire 调试配置:在 DEBUG 条件下调用 app.HangfireSetting()(可能是在开发环境启用 Dashboard 等)。
  • 公开的简单端点:
    • GET /ping -> "pong"
    • GET /datetime -> 当前时间与机器名
  • 启动日志并运行应用 app.Run()。
  • 整个启动包裹在 try/catch:若抛异常写入控制台并通过 Serilog.Log.Fatal 记录。
  1. Program 类常量
  • 定义了两个公用常量 Cookie 名称:AuthorizeCookieName 与 AccountCookieName,用于认证/账号信息存放(可能在别处被引用)。

关键配置细节回顾

  • Rate limiter "comment": 短期限制为 3 次/分钟,允许 2 个排队请求。
  • Kestrel 最大请求体:int.MaxValue(意味着几乎无限制)。
  • 输出缓存默认策略:10 分钟过期。
  • OpenAPI 文档自动把 README.md 加入文档描述,并添加了全局 Bearer 安全定义。
  • JWT 验证使用外部授权服务器(Authority),audience = "resource_server"。
  • 响应压缩包含比较多的 MIME 类型,压缩级别为最佳。

几点注意/建议

  • Forwarded headers 的 KnownProxies/KnownNetworks 被清空:这会使应用信任来自任意代理的 X-Forwarded-* 头。在直接公开 Internet 的场景下可能导致假冒 IP 等安全问题。通常应仅在受控的反向代理(比如 Nginx/负载均衡)前使用,或将可信代理 IP 加入 KnownProxies。
  • OpenAPI 中 SecuritySchemeType 使用了 OAuth2,但同时设置了 Bearer、Scheme、BearerFormat,这种混合类型可能导致 Swagger/客户端工具识别异常。若只使用 Bearer token,SecuritySchemeType.Http 且 Scheme = "bearer"、BearerFormat = "JWT" 更合适;若真的是 OAuth2,应明确配置 OAuth2 流程(authorizationUrl、tokenUrl 等)。
  • Kestrel 将 MaxRequestBodySize 设为 int.MaxValue 需谨慎:可能增加内存/磁盘压力或被滥用上传超大文件。应结合请求验证与限制策略。
  • Rate limiter 策略的名字 "comment" 暗示用于某些接口(例如发表评论),需在具体 Controller/Endpoint 上通过 [EnableRateLimiting("comment")] 或类似方式应用此策略。

总结 这段代码完成了一个功能较全的 Web API 启动脚本:配置中心、日志、认证授权、速率限制、压缩、缓存、OpenAPI 文档与 UI、后台任务(Hangfire)等都已集成并在中间件管道里就绪。许多具体行为(如服务注册、某些中间件实现)依赖于项目自定义扩展方法(AddDefaultServices、AddBusinessServices、AddPermissionAuthorization、UseRequestRecord 等),需要查看这些扩展的实现以获得完整细节。若需要,我可以进一步解释某一块(例如 JWT 配置、RateLimiter 的使用方法或 OpenAPI 安全配置的修正建议)。

评论加载中...