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();
}
报告:src/Dpz.Core.Auth/Program.cs
一、文件定位
这是 Dpz.Core 统一认证中心(OpenID Connect / Cookie 登录 / Passkey / MFA 宿主)的最小托管(Minimal Hosting)启动入口,采用顶层语句(Top-level statements)写法。它本身几乎不含业务逻辑,职责是编排:
- 引导日志(Bootstrap Logger)与日志后端(Serilog → 文件 / 控制台 / Seq);
- 配置来源(AgileConfig 配置中心 + 开发环境本地覆盖);
- 反向代理、CORS、限流、机器人拦截等宿主级策略;
- 通过一系列扩展方法注册业务、基础设施、OIDC、数据保护、MVC/API 服务;
- 组装 HTTP 请求管线(中间件顺序)与终结点,并在启动期完成一次性初始化;
- 全局异常兜底与日志刷盘。
配合 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.cs | IHttpContextAccessor + IHttpCurrentUserService |
AddBotProtection(o => o.CheckEmptyUserAgent = false) | BotProtectionExtensions.cs | 机器人拦截;此处按注释关闭 UA 判定(认证客户端复杂) |
AddS3ObjectStorage() | 命名空间 Dpz.Core.Service.ObjectStorage(未在本文件确认具体文件) | 腾讯云 COS / S3 兼容对象存储 |
AddOidc(configuration) | OpenIddictRegister.cs | OpenIddict 服务端 + 校验、Cookie 默认方案、IdentityCore、各 Store 与 Claims 工厂 |
认证中心自有的应用服务注册(在本项目内可直接定位):
IUserMfaVerificationService→ UserMfaVerificationService.cs;IUserPasskeySessionService→ UserPasskeySessionService.cs;AuthAssetService、AuthShellRenderService→ AuthAssetService.cs、AuthShellRenderService.cs(后者负责动态渲染 HTML 外壳、写入request-verification-tokenmeta 与 JSON 页面模型);AuthAssetOptions(配置节AuthAssets,类定义见 AuthAssetOptions.cs)与WebAuthnOptions(配置节WebAuthn,见 WebAuthnOptions.cs);IFido2单例:直接读取WebAuthn节构造Fido2Configuration(RPID = ServerDomain、RPName = ServerName、Origins规范化为忽略大小写的集合),配置缺失时抛InvalidOperationException。
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(); 之后的顺序(本文件原文顺序):
- 非开发环境
UseHsts(); UseForwardedHeaders()(让后续Request.Scheme/Host、限流取 IP 正确);- 内联中间件:读取 Cookie
SessionId,缺失则以 HttpOnly + Secure + SameSite=Strict 写入 1 年有效的Guid(用于匿名会话追踪); await app.UseRegisterInitApplicationAsync()——这不是中间件,而是 OpenIddictRegister.cs 中的一次性初始化:开 Scope 取IOpenIddictApplicationManager,当test客户端已存在或AgileConfig:env == "PROD"时直接返回;否则创建测试客户端test(含回调地址、全部端点/授权类型/响应类型/Scope 权限、PKCE 要求等)。因此它是启动期副作用,生产环境被显式短路;UseSerilogRequestLogging(...):EnrichDiagnosticContext注入RequestHost、RequestScheme、UserAgent、IpAddress(GetIpAddress()扩展)以及存在时的Referer;UseStaticFiles();UseRouting();UseCors(originsName);UseAuthentication();UseCurrentUserRequiredExceptionHandling()(CurrentUserRequiredExceptionMiddleware.cs);UseSecurityStampValidation()(SecurityStampValidationMiddleware.cs):对已认证请求用IMediator发ValidateUserSecurityStampRequest,并检查IAccountSessionService.IsRevokedAsync(SessionId);失败时SignOutAsync并重定向到/auth-status/...各状态页;UseAuthStatusPages()(AuthStatusPageMiddleware.cs):在await next之后拦截 403/404/500,仅对 HTML 请求(且排除/api、/connect/token等协议端点)渲染外壳页;404 还会经IIpRateLimitService.RecordEventAsync(ip, "404请求")记录事件(跳过IgnoreRequestPrefix);UseAuthAssetsRedirect()(AuthAssetsRedirectMiddleware.cs):生产环境下 404 的/auth-assetsGET/HEAD 请求 308 跳到AuthAssets:AssetsHost(Vite 动态 import 的 CSS 绝对路径只存在于 CDN);UseIpRateLimit()→UseBotProtection()→UseAuthorization();顺序与 Infrastructure/RateLimiting/README.md 的约定一致(限流需在UseAuthentication之后,以便已登录用户放行;且在机器人拦截之前);- 终结点:
MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}")、MapGet("/ping", ...)(回显请求头,健康探测)、UseDevToolsEndpoint()与UseVersionEndpoint()(实现均在 AspNetCoreServiceCollectionExtensions.cs:前者仅在开发环境暴露 Chrome DevTools workspace JSON,后者暴露/api/version); 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() 启用,因此与浏览器版本相关的拦截逻辑不会在此生效。