using Serilog.Events;

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

    // 开发环境下重新加载本地配置,覆盖配置中心的特定节点
    if (builder.Environment.IsDevelopment())
    {
        builder.Configuration.AddJsonFile(
            "appsettings.Development.json",
            optional: true,
            reloadOnChange: true
        );
    }

    var services = builder.Services;
    var configuration = builder.Configuration;

    var logSeq = configuration.GetSection("LogSeq").Get<LogSeq>();
    // ReSharper disable once RedundantAssignment
    Func<LogEvent, bool>? filter = null;
#if DEBUG
    filter = x =>
    {
        if (
            !x.Properties.TryGetValue("RequestPath", out var requestPath)
            || requestPath is not ScalarValue pathValue
        )
        {
            return false;
        }

        var pathPrefixes = new[] { "/auth-assets" };
        var path = pathValue.Value?.ToString();
        return path != null
            && pathPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
    };
#endif
    builder.Host.ConfigurationLog(logSeq, filter);

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        // Docker 网桥
        options.KnownIPNetworks.Add(new(IPAddress.Parse("172.17.0.0"), 16));
        // 显式信任单个代理 IP(避免 IPv6 映射问题)
        options.KnownProxies.Add(IPAddress.Parse("172.17.0.1"));
    });

    const string originsName = "ConfigurationOrigins";
    services.AddCors(options =>
    {
        options.AddPolicy(
            originsName,
            cfg =>
            {
                var origins = configuration.GetSection("Origins").Get<string[]>();
                if (origins is { Length: > 0 })
                {
                    cfg.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
                }
            }
        );
    });

    #region register inject
    services.AddBusinessServices(configuration);
    services.AddDefaultServices();
    services.AddDpzAspNetCoreServices();
    // 机器人拦截:认证客户端复杂,不做 UA/浏览器判定,仅拦截规则
    services.AddBotProtection(o => o.CheckEmptyUserAgent = false);
    services.AddHttpClient("edge");
    services.AddScoped<IUserMfaVerificationService, UserMfaVerificationService>();
    services.AddScoped<IUserPasskeySessionService, UserPasskeySessionService>();
    services.AddScoped<AuthAssetService>();
    services.AddScoped<AuthShellRenderService>();
    services.Configure<AuthAssetOptions>(configuration.GetSection("AuthAssets"));
    services.Configure<WebAuthnOptions>(configuration.GetSection("WebAuthn"));
    services.AddSingleton<IFido2>(_ =>
    {
        var webAuthn =
            configuration.GetSection("WebAuthn").Get<WebAuthnOptions>()
            ?? throw new InvalidOperationException("Missing WebAuthn configuration");
        return new Fido2(
            new Fido2Configuration
            {
                RPID = webAuthn.ServerDomain,
                RPName = webAuthn.ServerName,
                Origins = webAuthn.Origins.ToHashSet(StringComparer.OrdinalIgnoreCase),
            }
        );
    });
    //添加 腾讯云COS储存 S3兼容
    services.AddS3ObjectStorage();

    #endregion

    services.AddOidc(configuration);
    const string appName = "Dpz.Core.Auth";
    services.AddDataProtection().SetApplicationName(appName);
    services
        .AddOptions<KeyManagementOptions>()
        .Configure<IServiceScopeFactory>(
            (options, factory) =>
            {
                options.XmlRepository = new XmlRepositoryService(factory, appName + ".Key");
            }
        );

    services.AddAntiforgery(options => options.HeaderName = "RequestVerificationToken");
    services.AddControllersWithViews();
    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var message = string.Join(
                ";",
                context
                    .ModelState.Values.SelectMany(x => x.Errors)
                    .Select(x => x.ErrorMessage)
                    .Where(x => !string.IsNullOrWhiteSpace(x))
            );
            return new BadRequestObjectResult(ResponseResult.Fail(message));
        };
    });

    services.AddPermissionAuthorization();

    // 设备识别
    services.AddDetection();

    // 注册限流服务
    services.AddIpRateLimit(configuration);

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseForwardedHeaders();

    app.Use(
        async (context, next) =>
        {
            var sessionId = context.Request.Cookies["SessionId"];
            if (string.IsNullOrWhiteSpace(sessionId))
            {
                context.Response.Cookies.Append(
                    "SessionId",
                    Guid.NewGuid().ToString(),
                    new CookieOptions
                    {
                        HttpOnly = true,
                        Secure = true,
                        SameSite = SameSiteMode.Strict,
                        IsEssential = true,
                        Expires = DateTimeOffset.Now.AddYears(1),
                    }
                );
            }
            await next.Invoke();
        }
    );

    await app.UseRegisterInitApplicationAsync();

    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.UserAgent.ToString());
            diagnosticContext.Set("IpAddress", httpContext.Request.GetIpAddress());

            var referer = httpContext.Request.Headers.Referer;
            if (!StringValues.IsNullOrEmpty(referer))
            {
                diagnosticContext.Set("Referer", referer);
            }
        };
    });

    // app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCors(originsName);

    app.UseAuthentication();
    app.UseCurrentUserRequiredExceptionHandling();
    // 添加SecurityStamp验证中间件
    app.UseSecurityStampValidation();
    // 处理非正常响应的中间件
    app.UseAuthStatusPages();
    // 处理前端资源重定向的中间件
    app.UseAuthAssetsRedirect();
    // 添加IP限流中间件
    app.UseIpRateLimit();
    app.UseBotProtection();
    app.UseAuthorization();

    app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    app.MapGet(
        "/ping",
        requestDelegate: async context =>
        {
            await context.Response.WriteAsJsonAsync(context.Request.Headers);
        }
    );
    app.UseDevToolsEndpoint();
    app.UseVersionEndpoint();

    app.Run();
}
catch (Exception e)
{
    Console.Error.WriteLine(e);
    Log.Fatal(e, "Host terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

报告:src/Dpz.Core.Auth/Program.cs

一、文件定位

这是 Dpz.Core 统一认证中心(OpenID Connect / Cookie 登录 / Passkey / MFA 宿主)的最小托管(Minimal Hosting)启动入口,采用顶层语句(Top-level statements)写法。它本身几乎不含业务逻辑,职责是编排:

  1. 引导日志(Bootstrap Logger)与日志后端(Serilog → 文件 / 控制台 / Seq);
  2. 配置来源(AgileConfig 配置中心 + 开发环境本地覆盖);
  3. 反向代理、CORS、限流、机器人拦截等宿主级策略;
  4. 通过一系列扩展方法注册业务、基础设施、OIDC、数据保护、MVC/API 服务;
  5. 组装 HTTP 请求管线(中间件顺序)与终结点,并在启动期完成一次性初始化;
  6. 全局异常兜底与日志刷盘。

配合 Usings.cs 的 global using,文件中出现的大量类型(LogSeq、WebAuthnOptions、AuthAssetOptions、IMediator、IFido2、限流/机器人相关扩展)都无需在本文件显式 using。


二、启动阶段逐段说明

1) 引导日志

Log.Logger = new LoggerConfiguration().Enrich.FromLogContext().CreateBootstrapLogger(); 在 DI 尚未构建前就建立 Serilog 引导记录器,用于捕获启动期异常;配合 finally { Log.CloseAndFlush(); } 保证退出前刷盘。真正的日志后端配置在 builder.Host.ConfigurationLog(...)(见下)。

2) 配置中心与本地覆盖

  • builder.Host.UseAgileConfig(new ConfigClient(builder.Configuration)):接入 AgileConfig 配置中心;
  • 仅开发环境再 AddJsonFile("appsettings.Development.json", optional, reloadOnChange),用本地文件覆盖配置中心的特定节点(注释亦如此说明)。

3) 日志后端与 LogSeq

var logSeq = configuration.GetSection("LogSeq").Get<LogSeq>(); → builder.Host.ConfigurationLog(logSeq, filter)。 该扩展位于 LogConfiguration.cs,需要留意它与本文件的耦合约束:

  • 它内部会把传入的 filter 作为 Filter.ByExcluding(filter) 使用——即返回 true 的事件被排除,同时作用于控制台与控制台之外的 Seq 输出;
  • 若 logSeq 为 null 或缺少 ServerUri / Program(非 DEBUG 时还要求 ApiKey),该方法会直接抛 ArgumentNullException,从而落到本文件 catch 分支;
  • 本文件在 #if DEBUG 下传入的过滤器只排除 /auth-assets 前缀的请求路径日志(本地开发时静态资源请求噪音);非 DEBUG 时 filter 保持 null,由 LogConfiguration 的默认 LogFilter 排除 /runtask、/account、/notification、/app/notification。因此 // ReSharper disable once RedundantAssignment 是针对 filter 初值赋值的一处抑制。

4) 反向代理与 CORS

  • ForwardedHeadersOptions:仅信任 X-Forwarded-For / X-Forwarded-Proto,并把 Docker 网桥 172.17.0.0/16 加入 KnownIPNetworks、172.17.0.1 加入 KnownProxies(注释说明是为规避 IPv6 映射问题)。这与 OpenIddictRegister.cs 中 Issuer 的注释相呼应:处于反向代理之后必须正确转发 X-Forwarded-*。
  • CORS 策略名 ConfigurationOrigins,来源从配置节 Origins 读取;仅当非空时才 WithOrigins(...).AllowAnyMethod().AllowAnyHeader().AllowCredentials()。注意该策略在管线中通过 app.UseCors(originsName) 显式应用。

5) 服务注册区(#region register inject)

按调用链分工:

调用已确认的实现位置作用(依据源码)
AddBusinessServices(configuration)ServiceExtensions.cs注册 IUnitOfWork/IRepository<>、Mongo 索引初始化、浏览计数刷库、Redis/FusionCache、分布式锁等业务与基础设施底座
AddDefaultServices()ServiceDependencyInjection.cs注册源生成器产出的仓储等服务
AddDpzAspNetCoreServices()AspNetCoreServiceCollectionExtensions.csIHttpContextAccessor + IHttpCurrentUserService
AddBotProtection(o => o.CheckEmptyUserAgent = false)BotProtectionExtensions.cs机器人拦截;此处按注释关闭 UA 判定(认证客户端复杂)
AddS3ObjectStorage()命名空间 Dpz.Core.Service.ObjectStorage(未在本文件确认具体文件)腾讯云 COS / S3 兼容对象存储
AddOidc(configuration)OpenIddictRegister.csOpenIddict 服务端 + 校验、Cookie 默认方案、IdentityCore、各 Store 与 Claims 工厂

认证中心自有的应用服务注册(在本项目内可直接定位):

6) 数据保护与密钥持久化

const string appName = "Dpz.Core.Auth";
services.AddDataProtection().SetApplicationName(appName);
services.AddOptions<KeyManagementOptions>()
        .Configure<IServiceScopeFactory>((options, factory) =>
            options.XmlRepository = new XmlRepositoryService(factory, appName + ".Key"));

应用名固定为 Dpz.Core.Auth(保证多实例间密钥环一致),并用自定义 XmlRepositoryService(XmlRepositoryService.cs)替换默认密钥环存储——结合 IServiceScopeFactory 说明密钥通过仓储层(数据库)而非本地文件系统持久化;appName + ".Key" 作为存储命名空间。

7) MVC / API 行为

  • services.AddAntiforgery(options => options.HeaderName = "RequestVerificationToken"):与 AuthShellRenderService 写入的 meta 名称、前端 AJAX 头一致;
  • AddControllersWithViews():MVC + Razor 视图(Views 目录);
  • ApiBehaviorOptions.InvalidModelStateResponseFactory:把模型校验错误拼接为 ResponseResult.Fail(message) 的 BadRequestObjectResult,统一 API 错误报文格式。ResponseResult 来自 Dpz.Core.Public.ViewModel(见 Usings.cs 全局引用,未在本文件进一步确认其定义位置);
  • AddPermissionAuthorization()(PermissionAuthorizationExtensions.cs)注册授权处理器/策略;
  • AddDetection():设备识别服务(注意:本文件只注册服务,管线中并未出现 app.UseDetection();该中间件在 Web/Program.cs 中才被调用,且需在 RejectBots 之前。关联到 BotProtectionOptions.CheckBrowserVersion 的说明见 BotProtectionOptions.cs);
  • AddIpRateLimit(configuration):IP 限流服务,使用方为 RateLimitMiddlewareExtensions.cs。

8) 构建与请求管线

var app = builder.Build(); 之后的顺序(本文件原文顺序):

  1. 非开发环境 UseHsts();
  2. UseForwardedHeaders()(让后续 Request.Scheme/Host、限流取 IP 正确);
  3. 内联中间件:读取 Cookie SessionId,缺失则以 HttpOnly + Secure + SameSite=Strict 写入 1 年有效的 Guid(用于匿名会话追踪);
  4. await app.UseRegisterInitApplicationAsync()——这不是中间件,而是 OpenIddictRegister.cs 中的一次性初始化:开 Scope 取 IOpenIddictApplicationManager,当 test 客户端已存在或 AgileConfig:env == "PROD" 时直接返回;否则创建测试客户端 test(含回调地址、全部端点/授权类型/响应类型/Scope 权限、PKCE 要求等)。因此它是启动期副作用,生产环境被显式短路;
  5. UseSerilogRequestLogging(...):EnrichDiagnosticContext 注入 RequestHost、RequestScheme、UserAgent、IpAddress(GetIpAddress() 扩展)以及存在时的 Referer;
  6. UseStaticFiles();
  7. UseRouting();
  8. UseCors(originsName);
  9. UseAuthentication();
  10. UseCurrentUserRequiredExceptionHandling()(CurrentUserRequiredExceptionMiddleware.cs);
  11. UseSecurityStampValidation()(SecurityStampValidationMiddleware.cs):对已认证请求用 IMediator 发 ValidateUserSecurityStampRequest,并检查 IAccountSessionService.IsRevokedAsync(SessionId);失败时 SignOutAsync 并重定向到 /auth-status/... 各状态页;
  12. UseAuthStatusPages()(AuthStatusPageMiddleware.cs):在 await next 之后拦截 403/404/500,仅对 HTML 请求(且排除 /api、/connect/token 等协议端点)渲染外壳页;404 还会经 IIpRateLimitService.RecordEventAsync(ip, "404请求") 记录事件(跳过 IgnoreRequestPrefix);
  13. UseAuthAssetsRedirect()(AuthAssetsRedirectMiddleware.cs):生产环境下 404 的 /auth-assets GET/HEAD 请求 308 跳到 AuthAssets:AssetsHost(Vite 动态 import 的 CSS 绝对路径只存在于 CDN);
  14. UseIpRateLimit() → UseBotProtection() → UseAuthorization();顺序与 Infrastructure/RateLimiting/README.md 的约定一致(限流需在 UseAuthentication 之后,以便已登录用户放行;且在机器人拦截之前);
  15. 终结点:MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}")、MapGet("/ping", ...)(回显请求头,健康探测)、UseDevToolsEndpoint() 与 UseVersionEndpoint()(实现均在 AspNetCoreServiceCollectionExtensions.cs:前者仅在开发环境暴露 Chrome DevTools workspace JSON,后者暴露 /api/version);
  16. app.Run();然后 catch 中 Console.Error.WriteLine + Log.Fatal,finally 中 Log.CloseAndFlush()。

三、流程图

flowchart TD
    A["Bootstrap Logger<br/>CreateBootstrapLogger()"] --> B["WebApplication.CreateBuilder(args)"]
    B --> C["UseAgileConfig(ConfigClient)<br/>接入配置中心"]
    C --> D{"IsDevelopment?"}
    D -->|"是"| E["AddJsonFile appsettings.Development.json<br/>覆盖配置中心节点"]
    D -->|"否"| F["跳过本地覆盖"]
    E --> G["LogSeq 配置 + ConfigurationLog(filter)<br/>文件 / 控制台 / Seq,filter 为排除式"]
    F --> G
    G --> H["ForwardedHeadersOptions<br/>信任 172.17.0.0/16 与 172.17.0.1"]
    H --> I["Cors 策略 ConfigurationOrigins<br/>来源配置节 Origins"]
    I --> J["业务与基础设施注册<br/>AddBusinessServices / AddDefaultServices<br/>AddDpzAspNetCoreServices / AddBotProtection<br/>AddHttpClient(edge)"]
    J --> K["认证中心自有服务<br/>MFA / Passkey / AuthAsset / AuthShell / Fido2"]
    K --> L["AddOidc(configuration)"]
    L --> M["DataProtection(appName=Dpz.Core.Auth)<br/>XmlRepositoryService 密钥环"]
    M --> N["Antiforgery + ControllersWithViews<br/>+ InvalidModelStateResponseFactory"]
    N --> O["AddPermissionAuthorization / AddDetection / AddIpRateLimit"]
    O --> P["builder.Build()"]
    P --> Q["UseRegisterInitApplicationAsync()<br/>非 PROD 且 test 客户端不存在时创建测试客户端"]
    Q --> R["中间件管线与终结点映射"]
    R --> S["app.Run()"]
    S -->|"异常"| T["Log.Fatal + Console.Error"]
    S -->|"正常/异常退出"| U["finally: Log.CloseAndFlush()"]

请求管线顺序:

flowchart LR
    R0["UseHsts<br/>(非开发)"] --> R1["UseForwardedHeaders"]
    R1 --> R2["SessionId Cookie 中间件"]
    R2 --> R3["UseSerilogRequestLogging<br/>Enrich: Host/Scheme/UA/IP/Referer"]
    R3 --> R4["UseStaticFiles"]
    R4 --> R5["UseRouting"]
    R5 --> R6["UseCors(ConfigurationOrigins)"]
    R6 --> R7["UseAuthentication"]
    R7 --> R8["UseCurrentUserRequiredExceptionHandling"]
    R8 --> R9["UseSecurityStampValidation<br/>校验 SecurityStamp / 会话撤销"]
    R9 --> R10["UseAuthStatusPages<br/>403/404/500 渲染外壳页"]
    R10 --> R11["UseAuthAssetsRedirect<br/>/auth-assets 404 跳 CDN"]
    R11 --> R12["UseIpRateLimit"]
    R12 --> R13["UseBotProtection"]
    R13 --> R14["UseAuthorization"]
    R14 --> R15["MapControllerRoute default"]
    R15 --> R16["/ping、DevTools、/api/version"]

说明:UseAuthStatusPages、UseAuthAssetsRedirect、UseSecurityStampValidation、内联 SessionId 中间件都采用“先 await next 再处理响应/上下文”的写法;由于 UseRouting 已显式调用,终结点执行位于用户注册中间件之后,因此这些中间件能观察到控制器/终结点产生的响应状态。


四、与其它文件的协作关系小结

  • 配置来源:AgileConfig 提供主配置,开发环境用本地 JSON 覆盖;LogSeq、Origins、AuthAssets、WebAuthn、Server:*、IgnoreRequestPrefix 等节点分别被本文件与其调用的扩展读取。
  • 认证协议:OpenIddictRegister.cs 同时承担 DI 注册(AddOidc)与启动期客户端初始化(UseRegisterInitApplicationAsync),其 Issuer/证书相关约束直接影响到本文件中的 ForwardedHeaders 配置。
  • 请求处理链:Middleware 下四个中间件与管线各阶段一一对应,并依赖 Security 的授权策略、Service 下的业务服务(MFA、Passkey、Assets/Shell 渲染、Claims 工厂等)。
  • 共享基础设施:AddBusinessServices/AddDefaultServices(ServiceExtensions.cs、ServiceDependencyInjection.cs)、限流与机器人拦截扩展(RateLimitMiddlewareExtensions.cs、BotProtectionExtensions.cs)、ASP.NET Core 公共端点(AspNetCoreServiceCollectionExtensions.cs)让本宿主与 Web / WebApi / Jobs 保持一致的启动写法(这几处 Program.cs 的前半段几乎同构)。
  • 前端资产:AuthAssets 配置通过 AuthAssetService/AuthShellRenderService 与 AuthAssetsRedirectMiddleware 协同,实现“开发用 wwwroot 静态文件、生产用 CDN”的双模式。

需要注意的隐含前提(均由源码确认,而非推测):LogSeq 配置必须完整,否则启动即抛异常;WebAuthn 节必须存在,否则 IFido2 单例首次解析时抛异常;AddDetection 注册的服务在本宿主管线中未通过 UseDetection() 启用,因此与浏览器版本相关的拦截逻辑不会在此生效。

评论加载中...