OData场景下IQueryable数据脱敏后保留脱敏字段过滤能力的方案
OData端点脱敏后无法过滤的解决方案
问题根因
你当前的实现存在两个核心问题导致过滤失效:
- 脱敏逻辑写在返回IQueryable的前置投影中,OData的
$filter/$orderby/$skip/$top等操作会追加在这个投影之后,要么是直接针对脱敏后的数据做匹配(无法命中原始数据中的内容),要么是因为脱敏表达式无法被EF翻译为SQL触发客户端求值,后续的过滤分页只会作用在已经加载到内存的少量数据上,结果不符合预期。 - 你的脱敏表达式存在冗余写法:
int.Parse((x.EmailAddress.IndexOf("@").ToString()))完全可以直接用x.EmailAddress.IndexOf("@")代替,多余的类型转换会进一步提升EF表达式翻译失败的概率。
解决方案
根据你的业务需求可以选择以下两种方案:
方案一:序列化阶段脱敏(推荐,适配绝大多数场景)
如果你的需求是允许用户基于原始数据做过滤,仅最终返回给前端的结果需要脱敏,直接把脱敏逻辑从IQueryable投影中移除,放到序列化阶段执行即可,OData的所有查询操作都会直接作用在数据库原始字段上,完全保留原生查询能力。
实现步骤:
- 定义对应敏感字段的序列化转换器,以System.Text.Json为例:
// 邮箱脱敏转换器 public class EmailDesensitizationConverter : JsonConverter<string> { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString(); public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { if (string.IsNullOrWhiteSpace(value)) { writer.WriteStringValue(string.Empty); return; } var atIndex = value.IndexOf('@'); var desensitizedEmail = $"{value[..3]}*****{value[atIndex..]}"; writer.WriteStringValue(desensitizedEmail); } } // 手机号脱敏转换器 public class MobileDesensitizationConverter : JsonConverter<string> { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString(); public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { if (string.IsNullOrWhiteSpace(value) || value.Length < 4) { writer.WriteStringValue(string.Empty); return; } var desensitizedMobile = $"*** *** {value[^4..]}"; writer.WriteStringValue(desensitizedMobile); } } // 身份证号脱敏转换器 public class IdNumberDesensitizationConverter : JsonConverter<string> { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString(); public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { if (string.IsNullOrWhiteSpace(value) || value.Length < 7) { writer.WriteStringValue(string.Empty); return; } var desensitizedId = $"{value[..3]} *** *** {value[^4..]}"; writer.WriteStringValue(desensitizedId); } }
- 在User实体的对应字段上标记转换器:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string SecondName { get; set; } [JsonConverter(typeof(EmailDesensitizationConverter))] public string EmailAddress { get; set; } [JsonConverter(typeof(MobileDesensitizationConverter))] public string MobilePhone { get; set; } [JsonConverter(typeof(IdNumberDesensitizationConverter))] public string IdentityNumber { get; set; } // 其他字段... }
- 简化你的Get接口,直接返回原始IQueryable即可:
public IQueryable<User> Get() { return _defaultContext.Users.AsQueryable(); }
这个方案同时解决了表达式翻译问题、过滤匹配问题,还可以全局复用脱敏逻辑,不用每个查询接口都写重复的投影代码。
方案二:修正投影表达式,适配仅允许过滤脱敏后内容的场景
如果你的安全要求是不允许用户基于原始敏感数据做过滤,仅能基于脱敏后的可见内容做匹配,只需要修正你的投影表达式,确保所有逻辑都可以被EF翻译为SQL即可:
public IQueryable<User> Get() { return _defaultContext.Users .Select(x => new User { Id = x.UserId, FirstName = x.FirstName, SecondName = x.SecondName, EmailAddress = x.EmailAddress != null ? x.EmailAddress.Substring(0, 3) + "*****" + x.EmailAddress.Substring(x.EmailAddress.IndexOf("@")) : string.Empty, MobilePhone = x.MobilePhone != null && x.MobilePhone.Length >=4 ? "*** *** " + x.MobilePhone.Substring(x.MobilePhone.Length - 4) : string.Empty, IdentityNumber = x.IdentityNumber != null && x.IdentityNumber.Length >=7 ? x.IdentityNumber.Substring(0, 3) + " *** *** " + x.IdentityNumber.Substring(x.IdentityNumber.Length - 4) : string.Empty, IdNumberTypeId = x.IdentityTypeId, DateOfBirth = x.DateOfBirth, CountryOfIssue = x.CountryOfIssue.IsoCode }); }
修正后所有表达式都可以被EF正常翻译为SQL,OData的过滤操作会直接在数据库层面基于脱敏后的计算值执行,不会触发客户端求值。
额外优化建议
如果需要区分不同角色的脱敏规则(比如管理员可以看到完整敏感数据,普通用户只能看到脱敏后内容),可以在序列化转换器中加入当前用户角色判断,或者使用OData的属性访问控制API,针对不同角色动态配置字段的返回规则。
内容的提问来源于stack exchange,提问作者Damian Jacobs
相关产品推荐
相关产品推荐

