.NET 6 Web API中NLog自定义布局渲染器未生效及请求体拦截问题
.NET 6 Web API中NLog拦截并脱敏请求体的解决方案
问题排查
你的自定义LayoutRenderer未被调用,主要是以下几个原因:
- 注册时机错误:原代码中在
LogManager.LoadConfiguration后注册渲染器,但builder.Host.UseNLog()会重新初始化NLog配置,导致之前的注册失效。 - 中间件顺序错误:自定义请求处理中间件需要在NLog相关中间件之前执行,确保处理后的请求体已存入
HttpContext.Items,NLog才能读取到。 - NLog配置未引用自定义渲染器:如果
nlog.config的layout中没使用${aspnet-request-posted-body-2},NLog不会触发该渲染器。 - 请求流读取限制:请求体流默认只能读取一次,未启用缓冲会导致后续无法读取。
完整实现步骤
1. 自定义脱敏请求体LayoutRenderer
using NLog.LayoutRenderers; using NLog.Web.LayoutRenderers; [LayoutRenderer("aspnet-request-posted-body-2")] public class SanitizedRequestBodyRenderer : AspNetLayoutRendererBase { protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { var httpContext = HttpContextAccessor.HttpContext; if (httpContext == null) return; if (httpContext.Items.TryGetValue("SanitizedRequestBody", out var sanitizedBody)) { builder.Append(sanitizedBody as string); } } }
2. 请求体脱敏中间件
实现请求体读取、脱敏逻辑,并将处理后的内容存入HttpContext.Items:
using System.Text.Json; public class RequestSanitizationMiddleware { private readonly RequestDelegate _next; // 敏感字段列表,可根据需求扩展 private readonly HashSet<string> _sensitiveKeys = new(StringComparer.OrdinalIgnoreCase) { "Password", "Pwd", "Secret", "Token", "CardNumber" }; public RequestSanitizationMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // 启用请求流缓冲,允许多次读取 context.Request.EnableBuffering(); string rawBody = null; try { // 读取原始请求体 using var reader = new StreamReader(context.Request.Body, leaveOpen: true); rawBody = await reader.ReadToEndAsync(); // 重置流位置,确保后续中间件可正常读取 context.Request.Body.Position = 0; string sanitizedBody = rawBody; var contentType = context.Request.ContentType?.Split(';')[0]; // 根据Content-Type进行脱敏处理 switch (contentType) { case "application/json": case "application/*+json": case "text/json": sanitizedBody = SanitizeJson(rawBody); break; case "application/x-www-form-urlencoded": sanitizedBody = SanitizeFormData(rawBody); break; } // 将脱敏后的内容存入上下文 context.Items["SanitizedRequestBody"] = sanitizedBody; } catch (Exception ex) { var logger = context.RequestServices.GetRequiredService<ILogger<RequestSanitizationMiddleware>>(); logger.LogError(ex, "Failed to sanitize request body"); context.Items["SanitizedRequestBody"] = rawBody ?? "Failed to read request body"; } await _next(context); } // JSON格式脱敏 private string SanitizeJson(string json) { try { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var sanitizedJson = JsonSerializer.Serialize(SanitizeJsonElement(root)); return sanitizedJson; } catch { // 解析失败返回原内容 return json; } } private JsonElement SanitizeJsonElement(JsonElement element) { if (element.ValueKind == JsonValueKind.Object) { var properties = element.EnumerateObject().ToList(); var obj = new Dictionary<string, JsonElement>(); foreach (var prop in properties) { if (_sensitiveKeys.Contains(prop.Name)) { obj[prop.Name] = JsonSerializer.SerializeToElement("xxx"); } else { obj[prop.Name] = SanitizeJsonElement(prop.Value); } } return JsonSerializer.SerializeToElement(obj); } else if (element.ValueKind == JsonValueKind.Array) { var array = element.EnumerateArray().Select(SanitizeJsonElement).ToList(); return JsonSerializer.SerializeToElement(array); } return element; } // 表单格式脱敏 private string SanitizeFormData(string formData) { var pairs = formData.Split('&') .Select(pair => pair.Split('=', 2)) .Where(parts => parts.Length == 2) .Select(parts => new KeyValuePair<string, string>( Uri.UnescapeDataString(parts[0]), Uri.UnescapeDataString(parts[1]))); var sanitizedPairs = pairs.Select(kvp => { if (_sensitiveKeys.Contains(kvp.Key)) { return $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString("xxx")}"; } return $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"; }); return string.Join('&', sanitizedPairs); } }
3. 修正Program.cs配置
调整注册顺序,确保自定义渲染器在NLog初始化前注册,中间件顺序正确:
var builder = WebApplication.CreateBuilder(args); // 1. 注册自定义LayoutRenderer,必须在UseNLog之前 LogManager.Setup() .SetupExtensions(ext => ext.RegisterLayoutRenderer<SanitizedRequestBodyRenderer>("aspnet-request-posted-body-2")); // 2. 配置NLog builder.Logging.ClearProviders(); builder.Host.UseNLog(); // 3. 添加控制器服务 builder.Services.AddControllers(); var app = builder.Build(); // 4. 中间件顺序:HTTPS → 自定义脱敏中间件 → 路由 → 授权 → 控制器 app.UseHttpsRedirection(); app.UseMiddleware<RequestSanitizationMiddleware>(); app.UseRouting(); app.UseAuthorization(); app.MapControllers(); app.Run();
4. 配置NLog.config
确保layout中引用自定义渲染器:
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true"> <extensions> <add assembly="NLog.Web.AspNetCore"/> </extensions> <targets> <target xsi:type="File" name="requestLog" fileName="logs/request-${shortdate}.log" layout="${longdate}|${level:uppercase=true}|${aspnet-request-method}|${aspnet-request-url}|${aspnet-request-posted-body-2}|${message}" /> </targets> <rules> <logger name="Microsoft.AspNetCore" minlevel="Info" writeTo="requestLog" /> <logger name="*" minlevel="Debug" writeTo="requestLog" /> </rules> </nlog>
注意事项
- 敏感字段列表可根据业务需求动态调整,支持大小写不敏感匹配。
- 若请求体过大,可考虑限制读取长度,避免内存占用过高。
- 确保NLog.Web.AspNetCore包已正确安装(版本需与.NET 6兼容)。
内容的提问来源于stack exchange,提问作者RoLYroLLs
相关产品推荐
相关产品推荐

