You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 6 Web API中NLog自定义布局渲染器未生效及请求体拦截问题

.NET 6 Web API中NLog拦截并脱敏请求体的解决方案

问题排查

你的自定义LayoutRenderer未被调用,主要是以下几个原因:

  1. 注册时机错误:原代码中在LogManager.LoadConfiguration后注册渲染器,但builder.Host.UseNLog()会重新初始化NLog配置,导致之前的注册失效。
  2. 中间件顺序错误:自定义请求处理中间件需要在NLog相关中间件之前执行,确保处理后的请求体已存入HttpContext.Items,NLog才能读取到。
  3. NLog配置未引用自定义渲染器:如果nlog.config的layout中没使用${aspnet-request-posted-body-2},NLog不会触发该渲染器。
  4. 请求流读取限制:请求体流默认只能读取一次,未启用缓冲会导致后续无法读取。

完整实现步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 09:30:23