You need to enable JavaScript to run this app. OData场景下IQueryable数据脱敏后保留脱敏字段过滤能力的方案
优惠活动
大模型
产品
解决方案
定价
更多

OData场景下IQueryable数据脱敏后保留脱敏字段过滤能力的方案

OData端点脱敏后无法过滤的解决方案

问题根因

你当前的实现存在两个核心问题导致过滤失效:

  1. 脱敏逻辑写在返回IQueryable的前置投影中,OData的$filter/$orderby/$skip/$top等操作会追加在这个投影之后,要么是直接针对脱敏后的数据做匹配(无法命中原始数据中的内容),要么是因为脱敏表达式无法被EF翻译为SQL触发客户端求值,后续的过滤分页只会作用在已经加载到内存的少量数据上,结果不符合预期。
  2. 你的脱敏表达式存在冗余写法:int.Parse((x.EmailAddress.IndexOf("@").ToString()))完全可以直接用x.EmailAddress.IndexOf("@")代替,多余的类型转换会进一步提升EF表达式翻译失败的概率。

解决方案

根据你的业务需求可以选择以下两种方案:

方案一:序列化阶段脱敏(推荐,适配绝大多数场景)

如果你的需求是允许用户基于原始数据做过滤,仅最终返回给前端的结果需要脱敏,直接把脱敏逻辑从IQueryable投影中移除,放到序列化阶段执行即可,OData的所有查询操作都会直接作用在数据库原始字段上,完全保留原生查询能力。

实现步骤:

  1. 定义对应敏感字段的序列化转换器,以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);
    }
}
  1. 在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; }
    // 其他字段...
}
  1. 简化你的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.19 06:15:07