如何在Delegating Handler中用Serilog和Destructurama.Attributed实现日志脱敏
解决方案
方案一:根据端点反序列化到对应实体类(复用Destructurama特性)
既然你已经在实体类上标记了[LogMasked]和[NotLogged],最直接的方式就是根据请求端点匹配对应的实体类型,将JSON反序列化为该类型后再记录日志,这样Destructurama的特性会自动生效。
实现步骤与代码示例
- 建立端点与请求/响应实体的映射关系
public static class EndpointEntityMapping { // 键:完整端点路径(需与路由一致,比如"/api/user/login"),值:对应请求实体类型 public static readonly Dictionary<string, Type> RequestTypeMap = new Dictionary<string, Type> { { "/api/user/login", typeof(LoginRequest) }, // 补充其他端点的映射... }; // 同理维护响应实体的映射 public static readonly Dictionary<string, Type> ResponseTypeMap = new Dictionary<string, Type> { { "/api/user/login", typeof(LoginResponse) }, // 补充其他端点的响应映射... }; }
- 修改Delegating Handler的日志逻辑,添加反序列化处理
private async void Log(HttpRequestMessage request, HttpResponseMessage response) { var endpoint = string.Join(string.Empty, request.RequestUri.Segments); var requestJson = await GetHttpRequestContentAsync(request); // 处理请求日志:匹配映射反序列化 if (EndpointEntityMapping.RequestTypeMap.TryGetValue(endpoint, out var requestType)) { var requestObj = JsonConvert.DeserializeObject(requestJson, requestType); _logger.Information("Request: {@request}", requestObj); // 脱敏特性自动生效 } else { // 无映射的端点,可直接记录原始JSON或做通用脱敏 _logger.Information("Request (unmapped): {request}", requestJson); } // 处理响应日志逻辑同理 var responseJson = await response.Content.ReadAsStringAsync(); if (EndpointEntityMapping.ResponseTypeMap.TryGetValue(endpoint, out var responseType)) { var responseObj = JsonConvert.DeserializeObject(responseJson, responseType); _logger.Information("Response: {@response}", responseObj); // 脱敏特性自动生效 } else { _logger.Information("Response (unmapped): {response}", responseJson); } }
注:示例用Newtonsoft.Json实现反序列化,若项目用System.Text.Json,替换为对应的JsonSerializer.Deserialize方法即可。
方案二:手动解析JObject实现脱敏(无需实体映射)
如果不想维护端点-实体的映射表,或需要处理动态JSON结构,可以直接解析JObject,手动对敏感字段做掩码或忽略处理。
实现步骤与代码示例
- 编写通用脱敏工具类
public static class JsonMaskingHelper { // 需要掩码的字段:键为字段名,值为保留的前置字符数 private static readonly Dictionary<string, int> MaskFields = new Dictionary<string, int> { { "LoginName", 3 }, // 补充其他需要掩码的字段... }; // 需要忽略不记录的敏感字段 private static readonly HashSet<string> IgnoreFields = new HashSet<string> { { "Password" }, { "Token" }, // 补充其他敏感字段... }; public static JToken MaskSensitiveData(JToken token) { if (token is JObject obj) { var maskedObj = new JObject(); foreach (var prop in obj.Properties()) { if (IgnoreFields.Contains(prop.Name)) continue; // 跳过不记录的字段 if (MaskFields.TryGetValue(prop.Name, out var showFirst)) { // 仅对字符串类型字段做掩码处理 if (prop.Value.Type == JTokenType.String) { var value = prop.Value.Value<string>(); if (!string.IsNullOrEmpty(value) && value.Length > showFirst) { maskedObj[prop.Name] = value.Substring(0, showFirst) + new string('*', value.Length - showFirst); } else { maskedObj[prop.Name] = prop.Value; } } else { maskedObj[prop.Name] = prop.Value; } } else { // 递归处理嵌套对象 maskedObj[prop.Name] = MaskSensitiveData(prop.Value); } } return maskedObj; } else if (token is JArray array) { // 递归处理数组内的每个元素 var maskedArray = new JArray(); foreach (var item in array) { maskedArray.Add(MaskSensitiveData(item)); } return maskedArray; } else { return token; } } }
- 修改日志逻辑,使用工具类处理JSON
private async void Log(HttpRequestMessage request, HttpResponseMessage response) { var requestJson = await GetHttpRequestContentAsync(request); if (!string.IsNullOrEmpty(requestJson)) { var requestJObj = JToken.Parse(requestJson); var maskedRequest = JsonMaskingHelper.MaskSensitiveData(requestJObj); _logger.Information("Request: {@request}", maskedRequest); } var responseJson = await response.Content.ReadAsStringAsync(); if (!string.IsNullOrEmpty(responseJson)) { var responseJObj = JToken.Parse(responseJson); var maskedResponse = JsonMaskingHelper.MaskSensitiveData(responseJObj); _logger.Information("Response: {@response}", maskedResponse); } }
方案对比
- 方案一:复用已有的实体特性,代码简洁无重复规则,适合接口数量固定、结构稳定的项目,但需要维护端点-实体映射表。
- 方案二:无需依赖实体类,灵活性更高,适合动态JSON或不想维护映射表的场景,但需手动维护脱敏规则,可能与实体特性规则重复。
内容的提问来源于stack exchange,提问作者darrenleeyx
相关产品推荐
相关产品推荐

