.NET 5.0 API如何实现带敏感数据脱敏的请求响应日志记录
.NET 5 API全局请求响应日志+敏感字段脱敏实现方案
核心特性读取方法
你需要的读取属性上自定义[SensitiveData]特性的逻辑,可通过反射实现,两种常用写法:
- 直接判断是否标记特性:
Attribute.IsDefined(属性实例, typeof(SensitiveDataAttribute)) - 读取特性实例(适合带参数的特性):
属性实例.GetCustomAttribute<SensitiveDataAttribute>() != null
完整实现步骤
1. 实现脱敏工具类
该类负责遍历对象属性,对标记了敏感特性的字段做替换处理:
using System.Reflection; using System.Text.Json; public static class SensitiveDataMasker { // 自定义脱敏替换符 private const string MaskString = "***"; public static string MaskObject(object obj) { if (obj == null) return string.Empty; var type = obj.GetType(); var result = new Dictionary<string, object>(); // 遍历所有公共实例属性 var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in properties) { // 检查是否为敏感字段 if (Attribute.IsDefined(prop, typeof(SensitiveDataAttribute))) { result[prop.Name] = MaskString; continue; } var value = prop.GetValue(obj); // 嵌套复杂类型可加递归逻辑处理,这里简单处理直接赋值 result[prop.Name] = value; } // 序列化脱敏后的对象返回 return JsonSerializer.Serialize(result); } }
2. 实现全局日志中间件
中间件可以全局捕获所有端点的请求和响应,注意要开启请求响应流的倒带,避免读取流后接口无法正常获取数据:
using Microsoft.AspNetCore.Http; using Serilog; using System.IO; using System.Threading.Tasks; public class RequestResponseLogMiddleware { private readonly RequestDelegate _next; public RequestResponseLogMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // 处理请求日志 context.Request.EnableBuffering(); var requestReader = new StreamReader(context.Request.Body); var requestContent = await requestReader.ReadToEndAsync(); context.Request.Body.Position = 0; string maskedRequest = requestContent; // 仅处理JSON类型的请求 if (!string.IsNullOrWhiteSpace(requestContent) && context.Request.ContentType?.Contains("application/json") == true) { try { var requestObj = JsonSerializer.Deserialize<object>(requestContent); maskedRequest = SensitiveDataMasker.MaskObject(requestObj); } catch { // 反序列化失败直接保留原始内容 } } Log.Information("请求日志: 路径={Path}, 方法={Method}, 内容={Content}", context.Request.Path, context.Request.Method, maskedRequest); // 处理响应日志 var originalResponseBody = context.Response.Body; using var responseStream = new MemoryStream(); context.Response.Body = responseStream; await _next(context); responseStream.Seek(0, SeekOrigin.Begin); var responseContent = await new StreamReader(responseStream).ReadToEndAsync(); responseStream.Seek(0, SeekOrigin.Begin); string maskedResponse = responseContent; if (!string.IsNullOrWhiteSpace(responseContent) && context.Response.ContentType?.Contains("application/json") == true) { try { var responseObj = JsonSerializer.Deserialize<object>(responseContent); maskedResponse = SensitiveDataMasker.MaskObject(responseObj); } catch { // 反序列化失败直接保留原始内容 } } Log.Information("响应日志: 路径={Path}, 状态码={StatusCode}, 内容={Content}", context.Request.Path, context.Response.StatusCode, maskedResponse); await responseStream.CopyToAsync(originalResponseBody); } }
3. 注册服务
在Startup.cs的Configure方法中注册中间件,注意要放在路由匹配之前:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 放在最开头,或者Serilog注册之后 app.UseMiddleware<RequestResponseLogMiddleware>(); app.UseRouting(); // 其他中间件... app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
测试效果
当接口返回你定义的Document对象时,日志输出中NationalId字段会自动脱敏:
响应日志: 路径=/api/document/123, 状态码=200, 内容={"Id":123,"NationalId":"***","Data":""}
优化提示
- 如果有嵌套对象的敏感字段,在
SensitiveDataMasker中增加递归处理逻辑即可 - 文件上传、form-data等非JSON请求可以增加跳过逻辑,避免影响正常业务
- 高并发场景可以把属性的特性检查结果做缓存,减少重复反射的性能开销
内容的提问来源于stack exchange,提问作者olamatre
相关产品推荐
相关产品推荐

