尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

dotnet-starter-kit 审计模块实战:从 Fluent 埋点、双通道缓冲到 DLQ 与多租户查询

dotnet-starter-kit 审计模块实战:从 Fluent 埋点、双通道缓冲到 DLQ 与多租户查询 dotnet-starter-kit 审计模块实战从 Fluent 埋点、双通道缓冲到 DLQ 与多租户查询【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit导读本指南围绕 fullstackhero 出品的 .NET Starter Kitdotnet-starter-kit中的Auditing模块展开系统讲解这套仅追加append-only审计轨迹的完整技术方案实体变更、安全事件、异常与 HTTP 活动的捕获方式channel 缓冲 后台批量落库 死信队列DLQ的异步持久化链路以及面向多租户的只读查询 API。读完本文你将掌握如何通过静态 Fluent API 手动埋点、如何配置双通道发布器与保留策略、如何利用[NoAudit]排除敏感端点以及如何安全地跨租户检索审计记录。模块概览与设计目标在 Modules.Auditing 模块 中审计被设计为四类事件的总入口实体变更EntityChange由 EF Core 拦截器在SaveChanges时自动捕获记录插入、更新、删除、软删除与恢复安全事件Security登录成功/失败、令牌签发/吊销、权限拒绝、策略失败、模拟登录impersonation开始/结束等走独立的合规通道HTTP 活动Activity通过中间件记录每次请求的路径、状态码、耗时、请求/响应体预览异常Exception记录异常类型、消息、堆栈顶部帧与关联数据。模块注册顺序为Order 300见 .agents/rules/modules/auditing.md并通过 Program.cs 中的moduleAssemblies数组与builder.AddModules(moduleAssemblies)挂载到宿主。核心实体与上下文为AuditRecord、AuditDbContext在途事件为AuditEnvelope。Contracts 层Modules.Auditing.Contracts暴露了IAuditClient、ISecurityAudit、IAuditPublisher、IAuditSink、IAuditDlqSink、IAuditEnricher、NoAuditAttribute及各类 payload 记录。查询侧全部是只读 API完整端点清单见Features/v1/目录或运行后访问/scalar。核心抽象与 Contracts 面AuditEnvelope统一事件载体所有审计事件在发布前都被包装为 AuditEnvelope它携带归一化的元数据与强类型 Payload字段说明IdGuid.CreateVersion7()生成的时序 GUIDOccurredAtUtc/ReceivedAtUtc事件发生时间与接收时间均强制转为 UTCEventType/Severity事件类型与严重级别整数/字节存储TenantId/UserId/UserName多租户与用户归属TraceId/SpanId来自Activity.Current的分布式追踪上下文CorrelationId/RequestId请求关联标识Source事件来源如Identity、AuditDbContext类型名Tags位掩码标签见下文Payload强类型 payload 对象事件类型、严重级别与标签枚举见 AuditEnums.csAuditEventTypeNone / EntityChange / Security / Activity / ExceptionAuditSeverityNone / Trace / Debug / Information / Warning / Error / Critical与标准日志级别对齐SecurityActionLoginSucceeded / LoginFailed / TokenIssued / TokenRevoked / PasswordChanged / RoleAssigned / RoleRevoked / PermissionDenied / PolicyFailed / ImpersonationStarted / ImpersonationEndedEntityOperationInsert / Update / Delete / SoftDelete / RestoreActivityKindHttp / BackgroundJob / Command / Query / IntegrationExceptionAreaApi / Worker / Ui / Infra / UnknownBodyCaptureFlagsNone / Request / Response / Both用于标记活动事件捕获了哪些 HTTP 体AuditTagFlagsPiiMasked / OutOfQuota / Sampled / RetainedLong / HealthCheck / Authentication / Authorization由发布链路自动打标例如命中配额拒绝时打OutOfQuota掩码生效时打PiiMasked。六大 Contracts 接口IAuditClient源码Scoped 服务提供WriteEntityChangeAsync、WriteSecurityAsync、WriteActivityAsync、WriteExceptionAsync四个强类型写入方法适合在 Handler/服务中手动埋点ISecurityAudit安全事件专用门面内置登录、令牌、模拟登录的便捷方法IAuditPublisher发布器抽象PublishAsync(IAuditEvent, ct)IAuditSink/IAuditDlqSink批量落库 sink 与死信 sinkIAuditEnricher发布前的事件增强钩子可补全租户/用户/追踪信息或规范字段。静态 Audit Fluent API手动埋点模块提供静态入口 Audit.cs支持四种工厂方法// 安全事件登录失败自动得到 Warning 级别 await Audit.ForSecurity(SecurityAction.LoginFailed) .WithUser(userId, userName) .WithSecurityContext(subjectId: userId, clientId: web, authMethod: Password, reasonCode: bad-credentials, claims: new Dictionarystring, object? { [ip] ip }) .WriteAsync(ct); // 实体变更一般由拦截器自动产生 await Audit.ForEntityChange(IdentityDbContext, identity, Users, User, user.Id.ToString(), EntityOperation.Update, changes) .WithUser(currentUser.Id.ToString()) .WriteAsync(ct); // 活动 await Audit.ForActivity(ActivityKind.BackgroundJob, RetentionJob) .WithActivityResult(statusCode: 200, durationMs: elapsed) .WriteAsync(ct); // 异常 await Audit.ForException(ex, ExceptionArea.Api, routeOrLocation: /api/v1/users) .WriteAsync(ct);Builder还支持.WithTenant()、.WithTrace()、.WithCorrelation()、.WithRequestId()、.WithSource()、.WithTags()、.At(utc)等链式方法以及针对 payload 类型的类型化更新器如WithSecurityContext、WithActivityResult、WithEntityTransactionId。ForException会按异常类型自动推断严重级别OperationCanceledException→ InformationUnauthorizedAccessException→ Warning其余 → Error并截取最多 20 帧的堆栈顶部。启动期 Audit.Configure 与 enricher 原子快照静态 API 需要在启动时完成一次配置Audit.Configure(publisher, serializer, enrichers);在模块内由 AuditingConfigurator一个IHostedService在StartAsync中注入IAuditPublisher、IAuditSerializer和全部IEnumerableIAuditEnricher并调用Audit.Configure。关键注意点Gotchaenricher 列表被保存在private static volatile IAuditEnricher[]数组中Configure通过[.. enrichers]生成不可变快照并原子交换。WriteAsync内部只做一次 volatile 读取然后遍历这份不可变快照。因此绝不能在运行时直接修改线上 enricher 集合——否则会与增强循环产生集合已被修改的竞态。要调整 enricher应通过重新Configure整体替换快照。Enricher 的正确用法IAuditEnricher 在事件发布前被调用public sealed class TraceEnricher : IAuditEnricher { public void Enrich(IAuditEvent auditEvent) { /* 补全字段、规范化、上限控制 */ } }模块内置的JsonMaskingService实现IAuditMaskingService也是作为 scoped enricher 参与链路见 AuditingModule.cs。双通道 Channel 缓冲发布器请求永不被阻塞ChannelAuditPublisher 是默认的IAuditPublisher采用两条有界通道默认通道容量默认 50,000FullMode BoundedChannelFullMode.DropOldest。压力下丢弃最旧事件以保持延迟可预测适用于活动/实体变更这类海量但低合规要求的事件安全通道容量默认 50,000FullMode BoundedChannelFullMode.Wait。绝不丢弃——当安全通道满时PublishAsync会 await 写入用反压拖慢请求而不是丢事件。登录结果、权限变更、模拟登录都走这条通道。// 双通道初始化源码节选 _default Channel.CreateBoundedAuditEnvelope(new BoundedChannelOptions(capacity) { FullMode BoundedChannelFullMode.DropOldest, ... }); _security Channel.CreateBoundedAuditEnvelope(new BoundedChannelOptions(securityCapacity) { FullMode BoundedChannelFullMode.Wait, ... });发布时按envelope.EventType AuditEventType.Security分流默认通道因DropOldest永不返回 false代码通过比较Reader.Count capacity近似统计丢弃数写入 AuditingTelemetry.Dropped。发布前还会做两级上下文回填BackfillScopeContext / BackfillAmbientContext从IAuditScope回填租户与用户再从 Finbuckle 的IMultiTenantContextAccessor与Activity.Current回填 ambient 租户与 trace/span——这保证了在 Hangfire 作业等无 HTTP 请求的场景下事件仍能带上租户与追踪信息。后台批量写线程与 DLQAuditBackgroundWorker 是唯一消费者BackgroundService核心行为双通道排水每轮先排空安全通道让反压的发布者尽快解除阻塞再用默认通道补满批次一次 flush 分摊两条通道的 I/O批处理参数batchSize默认 200、flushIntervalMs默认 1000源码下限保护为 50ms重试策略主 sink 失败最多重试MaxRetries 3次退避从 150ms 起步、每次翻倍、封顶 2 秒InitialBackoff/MaxBackoff降级到 DLQ重试耗尽后整批交给IAuditDlqSink保证 PostgreSQL 宕机时事件不丢优雅停机ExecuteAsync结束后执行FinalFlushAsync尽力落盘停机期间取消重试且不进 DLQ强制终止时丢失可接受worker 自身异常只记日志、不拖垮宿主。FileAuditDlqSink文件死信队列默认 DLQ 实现 FileAuditDlqSink 将事件以 JSONL 追加写入{ContentRoot}/audit-dlq/audit-dlq-{yyyy-MM-dd}.jsonl按日轮转。它刻意不依赖 PostgreSQL、Redis 等基础设施——因为主 sink 失败的原因往往正是这些设施——运维可用 Filebeat/Vector 等工具将文件搬运出主机再离线回放。SqlAuditSink 的多租户分组写入SqlAuditSink 负责把批次写入数据库要点按TenantId分组对每组在全新 scope中解析租户TenantId null时落到 Root 租户见MultitenancyConstants.Root.Id并通过IMultiTenantContextSetter显式设置 ambient tenant 上下文——因为后台写线程本身没有 ambient tenant找不到租户的分组直接跳过并记 Warning每条事件序列化为AuditRecordPayloadJson由IAuditSerializer.SerializePayload生成落库为jsonb。表结构与索引配置在 AuditRecordConfiguration.cs表名AuditRecords、schemaaudit、IsMultiTenant()、PayloadJson用jsonb类型并为热路径设计了五组索引(TenantId, OccurredAtUtc)降序组合索引——默认列表页的 index-only 走查(TenantId, EventType, OccurredAtUtc)——仪表盘按事件类型切片CorrelationId、TraceId单列索引——这个请求到底发生了什么Source、UserName的pg_trgmGIN 索引——把%term%ILIKE 从顺序扫描变成探测PayloadJson的jsonb_path_opsGIN 索引——支撑 JSON 包含查询。AuditDbContext 还通过HasPostgresExtension(pg_trgm)声明扩展并将AuditJsonbFunctions.AsText翻译为CAST(x AS text)否则对jsonb直接 ILIKE 会抛like_escape(jsonb, unknown) does not exist。两个拦截器别混淆模块内有两个名字相近但职责完全不同的拦截器AuditingSaveChangesInterceptor本模块源码SavingChangesAsync时收集Added/Modified/Deleted状态的实体经 EntityDiffBuilder 生成属性级 diff按(DbContext, Schema, Table, EntityName, Key, Operation)分组发布为EntityChange事件并带上当前事务的TransactionId。它显式跳过AuditDbContext避免审计表自身的写入被再次审计形成递归每次 flush 都会捕获AuditRecord插入其PayloadJson又内嵌了上一次的事件最终撑爆序列化。AuditableEntitySaveChangesInterceptorBuildingBlocks 层见 src/BuildingBlocks/Persistence/Inteceptors/AuditableEntitySaveChangesInterceptor.cs负责给实体打审计/软删除字段创建人、创建时间、修改人、修改时间、是否删除等。不同的文件、不同的职责不要混用。HTTP 活动审计中间件与 NoAuditAuditHttpMiddleware 由 AuditingModule.ConfigureMiddleware 挂载覆盖每个请求命中排除前缀ExcludePathStartsWith或流式响应Accept: text/event-streamSSE/SignalR时直接放行——流式响应绝不能被缓冲否则长连接永不返回导致客户端挂死无[NoAudit]或BodyOnlytrue时捕获请求体/响应体预览并做 JSON 掩码记录状态码、耗时、大小最终发布ActivityKind.Http活动事件异常路径按 ExceptionSeverityClassifier 分类只有达到MinExceptionSeverity默认Error才写异常审计然后重抛自动打标HttpContext.Items[HttpContextItemKeys.QuotaRejected]为 true 时打OutOfQuota有字段被掩码时打PiiMasked。排除端点[NoAudit] 两种模式NoAuditAttribute 适用于因合规或隐私原因密码重置、支付表单、MFA 登记不能记录请求/响应体的端点BodyOnly false默认完全跳过审计连活动记录都不写BodyOnly true仍记录活动耗时、状态、来源、租户、用户但省略请求/响应体预览。// 方式一元数据 endpoints.MapPost(/reset-password, ...) .WithMetadata(new NoAuditAttribute()) .RequirePermission(...); // 方式二便捷扩展 endpoints.MapPost(/reset-password, ...).NoAudit();JSON 掩码敏感字段自动打码JsonMaskingService 按字段名关键词约定做掩码命中即替换为****password, secret, token, otp, pin, accessToken, refreshToken, apiKey, clientSecret, authCode, authorization, bearer, connectionString匹配规则为key.Contains(keyword, OrdinalIgnoreCase)递归遍历JsonObject与JsonArray无字段命中时返回原对象引用跳过额外的序列化一跳与PiiMasked标签JSON 解析失败时安全回退为不掩码。要增加敏感字段直接向MaskKeywords补充关键词即可。只读查询 API七个端点路由前缀为api/v{version:apiVersion}/audits版本 v1见 AuditingModule.MapEndpoints全部要求Permissions.AuditTrails.View权限AuditingPermissions.cs端点用途GET /api/v1/audits分页列出/搜索审计事件GET /api/v1/audits/{id}单条详情GET /api/v1/audits/by-correlation按 CorrelationId 聚合一次请求的全部事件GET /api/v1/audits/by-trace按 TraceId 追踪分布式调用链GET /api/v1/audits/security安全事件GET /api/v1/audits/exceptions异常事件GET /api/v1/audits/summary汇总统计端点实现见 Features/v1/Query/DTO 定义在 Contracts 的 v1/ 与 Dtos。GetAudits 的过滤参数GetAuditsQuery 支持分页PageNumber、PageSize、Sort时间窗FromUtc、ToUtc归属TenantId、UserId类型EventType与ExcludeEventType后者用 not-equals 过滤保证分页与总数正确常用于排除Activity噪音级别/标签/来源Severity、Tags位掩码与运算、Source关联CorrelationId、TraceId全文Search对PayloadJson、Source、UserName做 ILIKE。跨租户查询与窗口限制GetAuditsQueryHandler 内含两条重要安全边界跨租户必须显式授权请求的TenantId与当前租户不同时会校验Permissions.AuditTrails.ViewCrossTenant权限否则抛ForbiddenException通过后IgnoreQueryFilters()绕过 Finbuckle 的匿名租户过滤并重新显式施加TenantId谓词避免意外返回所有租户的数据时间窗口强制有界未传From/To时默认回看 7 天DefaultWindow窗口超过 90 天MaxWindow时被截断防止无约束查询退化为全表顺序扫描。保留策略与清理作业AuditRetentionOptions 由Auditing:Retention配置段绑定AuditRetentionJob 作为 Hangfire 定时任务执行MapEndpoints中无条件注册Enabledfalse时作业为 no-op配置项默认值说明Enabledfalse总开关默认关闭、需显式开启ActivityRetentionDays30活动事件保留天数短EntityChangeRetentionDays90实体变更保留天数SecurityRetentionDays365安全事件保留天数合规友好ExceptionRetentionDays180异常事件保留天数DeleteBatchSize5000每次ExecuteDeleteAsync删除的行数上限降低 Postgres 锁压力作业循环直到删除量小于批大小Cron30 3 * * *Hangfire cron默认每天 03:30 UTC配置项速查AuditingModule.ConfigureServices通过builder.Configuration.GetSection(Auditing)绑定 AuditHttpOptions通过Auditing:Retention绑定保留选项。未配置时使用源码中的默认值。可在appsettings.json中按需覆盖{ Auditing: { CaptureBodies: true, MaxRequestBytes: 8192, MaxResponseBytes: 16384, AllowedContentTypes: [ application/json, application/problemjson ], ExcludePathStartsWith: [ /health, /metrics, /_framework, /swagger, /scalar, /openapi ], MinExceptionSeverity: Error, Retention: { Enabled: false, ActivityRetentionDays: 30, EntityChangeRetentionDays: 90, SecurityRetentionDays: 365, ExceptionRetentionDays: 180, DeleteBatchSize: 5000, Cron: 30 3 * * * } } }要点说明CaptureBodiesfalse时连体预览都不捕获活动事件仅记录元数据ExcludePathStartsWith排除健康检查、指标、文档类端点避免审计噪音MinExceptionSeverity低于该级别的异常如OperationCanceledException被归类为 Information不会写异常审计。此外宿主 appsettings.json 的OpenTelemetryOptions.Metrics.MeterNames已包含FSH.Modules.Auditing发布器与 worker 的 Published/Dropped/Flushed/FlushFailed/DeadLettered/FlushDurationMs 指标会随 OpenTelemetry 一同导出。从源码结构看生产实践建议给安全事件留足容量并监控反压安全通道是Wait模式DropOldest永不生效当AuditBackgroundWorker的 sink 持续失败时发布线程会被反压拖慢这是有意的慢请求换不丢事件设计应通过指标告警敏感端点必须显式排除涉及密码、支付、MFA 的端点应加[NoAudit]完整跳过或.NoAudit(bodyOnly: true)保留活动元数据——仅靠关键词掩码不保证覆盖所有敏感字段掩码关键词按业务扩展在JsonMaskingService.MaskKeywords中补充自定义敏感字段名生产检索性能Search的 ILIKE 由(TenantId, OccurredAtUtc)索引限定扫描范围重文本检索场景应依赖PayloadJson的 GIN 索引或将高频查询字段反规范化DLQ 文件需离线回放audit-dlq/*.jsonl是最后防线建议配置日志采集器Filebeat/Vector搬运并离线重放而非期望应用内自动恢复保留策略按事件类型差异化安全事件 365 天、活动 30 天的默认组合已兼顾合规与表体积开启Auditing:Retention:Enabled后由 Hangfire 每日 03:30 UTC 清理。参考资料模块规则文档.agents/rules/modules/auditing.md模块实现src/Modules/Auditing/Modules.AuditingCore、Features/v1、Infrastructure、PersistenceContracts 层src/Modules/Auditing/Modules.Auditing.Contracts宿主注册src/Host/FSH.Starter.Api/Program.cs客户端查询示例clients/admin/src/api/audits.ts、clients/dashboard/src/api/audits.ts前端审计页面clients/admin/src/pages/audits、clients/dashboard/src/pages/audits.tsx【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表