在已有业务下面添加脱敏需求,不允许污染源方法,所以只能对返回值进行处理,这里列举两种方法,自定义序列化和过滤器。
自定义序列化应对所有用到该实体类的情况,过滤器应对只需要某一个或几个请求脱敏的情况;
如果仅仅处理脱敏,直接使用通用脱敏工具类即可。
通用脱敏工具类代码如下
1 public static class DesensitizationUtil 2 { 3 /// <summary> 4 /// 将传入的字符串中间部分字符替换成特殊字符 5 /// </summary> 6 /// <param name="value">需要替换的字符串</param> 7 /// <param name="startLen">前保留长度,默认4</param> 8 /// <param name="subLen">要替换的长度,默认4</param> 9 /// <param name="specialChar">特殊字符,默认为*</param> 10 /// <returns>替换后的结果</returns> 11 public static string ReplaceWithSpecialChar(this string value, int startLen = 4, int subLen = 4, char specialChar = '*') 12 { 13 if (value.Length <= startLen + subLen) return value; 14 15 string startStr = value.Substring(0, startLen); 16 string endStr = value.Substring(startLen + subLen); 17 string specialStr = new string(specialChar, subLen); 18 19 return startStr + specialStr + endStr; 20 } 21 }
方式一:JsonConverter
JsonConverter的方式对实体类进行修饰,所有用到该实体类的地方都会按照指定的格式输出。我们只需要输出时脱敏,所以将CanRead设置为false,关闭反序列化,同时ReadJson不填写逻辑代码;CanWrite设置为true,WriteJson中将返回值value脱敏后写入。
引用:
1 using Newtonsoft.Json;
具体代码:
1 public class DesensitizationConvter : JsonConverter 2 { 3 //关闭反序列化 4 public override bool CanRead => false; 5 //开启自定义序列化 6 public override bool CanWrite => true; 7 public override bool CanConvert(Type objectType) 8 { 9 return true; 10 } 11 private readonly int StartLen; 12 private readonly int SubLen; 13 private readonly char SpecialChar; 14 /// <summary> 15 /// 默认前保留长度4,脱敏长度4,特殊字符为* 16 /// </summary> 17 public DesensitizationConvter() 18 { 19 StartLen = 4; 20 SubLen = 4; 21 SpecialChar = '*'; 22 } 23 /// <summary> 24 /// 根据传入的长度进行脱敏 25 /// </summary> 26 /// <param name="startLen">前保留长度</param> 27 /// <param name="subLen">脱敏长度</param> 28 public DesensitizationConvter(int startLen, int subLen) : this() 29 { 30 StartLen = startLen; 31 SubLen = subLen; 32 } 33 /// <summary> 34 /// 根据传入的长度及特殊字符进行脱敏 35 /// </summary> 36 /// <param name="startLen">前保留长度</param> 37 /// <param name="subLen">脱敏长度</param> 38 /// <param name="specialChar">替换的特殊字符</param> 39 public DesensitizationConvter(int startLen, int subLen, char specialChar) : this(startLen, subLen) 40 { 41 SpecialChar = specialChar; 42 } 43 44 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 45 { 46 throw new NotImplementedException(); 47 } 48 49 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 50 { 51 if (value != null) 52 { 53 writer.WriteValue(value.ToString().ReplaceWithSpecialChar(StartLen, SubLen, SpecialChar)); 54 } 55 } 56 }
然后在实体类需要脱敏的属性上面加上修饰:
1 [JsonConverter(typeof(DesensitizationConvter))] 2 public string Phone { get; set; } 3 4 [JsonConverter(typeof(DesensitizationConvter), 3, 4)] 5 public string Phone2 { get; set; } 6 7 [JsonConverter(typeof(DesensitizationConvter), 4, 10, '*')] 8 public string IDCard { get; set; }
WebApi请设置序列化工具类为Newtonsoft.Json,直接序列化方式为JsonConvert.SerializeObject(model)。
方式二:过滤器(JObject遍历)
过滤器Filter的方式相比较于Json序列化更加灵活,哪个请求需要脱敏,就加上修饰,不影响其他请求结果。
引用:
1 using Newtonsoft.Json.Linq; 2 using System.Linq;
构造函数中为需要脱敏的对象赋值,这里为了更加灵活,使用的是动态传参,然后处理成二维数组,支持多个属性、自定义脱敏位置的传参方式,包含需要脱敏的字段名称、前保留长度、脱敏长度、脱敏的特殊符号。
具体代码:
1 public class DesensitizationAttribute : Attribute, IActionFilter 2 { 3 public string[][] Desensitizations { get; set; } 4 // 动态传参,可以多个字段脱敏 5 public DesensitizationAttribute(params string[] desensitizations) 6 { 7 Desensitizations = new string[desensitizations.Length][]; 8 for (int i = 0; i < desensitizations.Length; i++) 9 { 10 //处理中文逗号 11 desensitizations[i] = desensitizations[i].Replace(",", ","); 12 string[] str = desensitizations[i].Split(','); 13 //准备脱敏的参数 14 switch (str.Count()) 15 { 16 case 1: 17 Desensitizations[i] = new string[4] { str[0].Trim(), "4", "4", "*" }; 18 break; 19 case 3: 20 Desensitizations[i] = new string[4] { str[0].Trim(), str[1].Trim(), str[2].Trim(), "*" }; 21 break; 22 case 4: 23 Desensitizations[i] = new string[4] { str[0].Trim(), str[1].Trim(), str[2].Trim(), str[3].Trim() }; 24 break; 25 default: 26 break; 27 } 28 } 29 } 30 31 public void OnActionExecuting(ActionExecutingContext context) 32 { 33 34 } 35 36 public void OnActionExecuted(ActionExecutedContext context) 37 { 38 if (context.Result != null && context.Result is ObjectResult) 39 { 40 ObjectResult obj = context.Result as ObjectResult; 41 //转换成JObject 42 JObject jo = JObject.FromObject(obj.Value); 43 //如果已知需要替换的字段层级关系,直接查询脱敏即可,例如传入A.B.Phone 44 //foreach (var item in Desensitizations) 45 //{ 46 // if (jo.SelectToken(item[0]) != null) 47 // { 48 // string newStr = jo.SelectToken(item[0]).ToString().ReplaceWithSpecialChar(Convert.ToInt32(item[1]), Convert.ToInt32(item[2]), Convert.ToChar(item[3])); 49 // jo.SelectToken(item[0]).Replace(newStr); 50 // } 51 //} 52 53 // 如果不知道层级,使用下面方法遍历JObject 54 foreach (JToken item in jo.Values()){ 55 CommonReplaceConvter(item); 56 } 57 58 context.Result = new ObjectResult(jo); 59 } 60 } 61 62 /// <summary> 63 /// 递归遍历JToken脱敏 64 /// </summary> 65 /// <param name="jt"></param> 66 public void CommonReplaceConvter(JToken jt) 67 { 68 if (jt == null) return; 69 70 if (jt.HasValues && jt.Values().Count() > 0) 71 { 72 foreach (JToken item in jt.Values()) 73 { 74 CommonReplaceConvter(item); 75 } 76 } 77 else 78 { 79 string[] str = Desensitizations.FirstOrDefault(t => t[0] == jt.Path.Split('.').Last()); 80 81 if (str != null) 82 { 83 string result = jt == null ? "" : jt.ToString().ReplaceWithSpecialChar(Convert.ToInt32(str[1]), Convert.ToInt32(str[2]), Convert.ToChar(str[3])); 84 jt.Replace(result); 85 } 86 } 87 } 88 }
最后在Controller上添加修饰,第一个参数表示Phone字段按照默认格式脱敏;第二个参数表示Phone2字段前面保留3位,脱敏长度为4,特殊字符为默认的*;第三个参数表示IDCard字段前面保留4位,脱敏长度为10,特殊字符为*(也可以改成自定义的字符):
[Desensitization("Phone", "Phone2,3,4", "IDCard,4,10,*")]
方式三:过滤器(反射)
引用:
1 using System.Collections; 2 using System.Linq; 3 using System.Reflection;
此方法与第二种方法类似,只是替换的方式不同,这里用的是反射。
构造函数中为需要脱敏的对象赋值,这里用的是二维数组,包含需要脱敏的字段名称、前保留长度、脱敏长度、脱敏的特殊符号。具体传值方法请往下看。
具体代码:
1 public class DesensitizationAttribute : Attribute, IActionFilter 2 { 3 public string[][] Desensitizations { get; set; } 4 public DesensitizationAttribute(params string[] desensitizations) 5 { 6 Desensitizations = new string[desensitizations.Length][]; 7 for (int i = 0; i < desensitizations.Length; i++) 8 { 9 desensitizations[i] = desensitizations[i].Replace(",", ","); 10 string[] str = desensitizations[i].Split(','); 11 switch (str.Count()) 12 { 13 case 1: 14 Desensitizations[i] = new string[4] { str[0].Trim(), "4", "4", "*" }; 15 break; 16 case 3: 17 Desensitizations[i] = new string[4] { str[0].Trim(), str[1].Trim(), str[2].Trim(), "*" }; 18 break; 19 case 4: 20 Desensitizations[i] = new string[4] { str[0].Trim(), str[1].Trim(), str[2].Trim(), str[3].Trim() }; 21 break; 22 default: 23 break; 24 } 25 } 26 } 27 28 public void OnActionExecuting(ActionExecutingContext context) 29 { 30 31 } 32 33 public void OnActionExecuted(ActionExecutedContext context) 34 { 35 if (context.Result != null && context.Result is ObjectResult) 36 { 37 ObjectResult obj = context.Result as ObjectResult; 38 39 GetInfoPropertys(obj.Value); 40 context.Result = obj; 41 } 42 } 43 44 /// <summary> 45 /// 反射的方式递归脱敏 46 /// </summary> 47 /// <param name="obj">需要脱敏的对象</param> 48 public void GetInfoPropertys(object obj) 49 { 50 if (obj == null) return; 51 Type type = obj.GetType(); 52 if (type.IsGenericType) 53 { 54 //如果是List,需要遍历 55 if (obj is ICollection Ilist) 56 { 57 foreach (object o in Ilist) 58 { 59 GetInfoPropertys(o); 60 } 61 } 62 return; 63 } 64 foreach (PropertyInfo property in type.GetProperties()) 65 { 66 object value = property.GetValue(obj, null); 67 if (property.PropertyType.IsValueType || property.PropertyType.Name.StartsWith("String")) 68 { 69 string[] str = Desensitizations.FirstOrDefault(t => t[0] == property.Name); 70 if (str != null) 71 { 72 string result = value == null ? "" : value.ToString().ReplaceWithSpecialChar(Convert.ToInt32(str[1]), Convert.ToInt32(str[2]), Convert.ToChar(str[3])); 73 property.SetValue(obj, result, null); 74 } 75 } 76 else 77 { 78 GetInfoPropertys(value); 79 } 80 } 81 } 82 }
同样在Controller上添加修饰:
[Desensitization("Phone", "Phone2,3,4", "IDCard,4,10,*")]