1.背景
在当下互联网高速发展的时代下,涉及到用户的隐私数据安全越发重要,一旦泄露将造成不可估量的后果。所以现在的业务系统开发中都会对用户隐私数据加密之后存储落库,同时还要求后端返回数据给前台之前进行数据脱敏。所谓脱敏处理其实就是将数据进行混淆隐藏,如将用户的手机号脱敏展示为`178****5939,采用 * 进行隐藏,以免泄露个人隐私信息。其实我之前就总结过相关联的功能实现:《实现数据加密存储、模糊匹配和脱敏》 但是强调一下今天这里并不是重复再讲一遍,而是在之前总结的基础上进行延伸拓展,重点在于标题里的:动态灵活可配置。那什么是动态灵活可配置呢?且听我娓娓道来。
那是一个惬意的下午时光,我听着歌,敲着一手代码……,突然间产品就来到我桌前,打断了我短暂的文思泉涌高光时刻,企图给我安排一个折磨的活,打开了奶茶app,暗示没有一杯奶茶解决不了的需求。是的最后这活我接了,也就是我们今天所要讲的数据脱敏由之前的前端脱敏改为后端,当然脱敏功能本身并不复杂难实现,那为啥说它是折磨人的活呢?普通的脱敏功能大概是这样的,也就是脱敏上面所说的用户固定隐私数据:姓名、手机号、身份证号、地址、身份证...等
,但是我们的系统需求要求不只是前面的字段,还需要支持其他字段,一句话生动形象地总结概括就是客户想脱啥就脱啥,想在哪脱就在哪脱,包括我们系统支持的自定义字段,都可以通过配置进行脱敏,如下图所示:
由图可知,这个脱敏需求功能涉及到以下几点:
① 哪些字段可以脱敏是可配置的,脱敏规则也是可配置的,比如说什么开头、中间、结尾,区间啥的。
② 脱敏设计到组织架构过滤,也就是说需要实现某个部门下的用户看到的数据是脱敏的,某个部门下的用户看到的数据是不脱敏的。这感觉就像后端接口功能菜单权限检验,判断当前用户是否有调用某个功能菜单接口的权限。
③ 涉及到角色的判断,某些角色需要脱敏(小喽啰不给看,防止把客户数据卖了),而某些角色不需要(管理员随便看,随便卖)。
这里我们暂且不讨论脱敏功能这么设计是否合理,反正产品是这样要求实现的,按照上面的列出来的点感觉也还好,也不至于上升到折磨的程度,折磨的是系统的历史原因,要脱敏的字段信息遍布都整个业务系统表单,每个表单的接口数据有冗余的有共用的,这就意味着每个页面表单接口都需要去梳理一遍。同时脱敏字段还涉及到编辑更新功能,而且之前的更新接口都是一个表单整体提交,这就导致一个脱敏字段没有修改,但是前端把脱敏数据传回后端来,又是一个一个去适配啊~,难顶。闹骚发完了,言归正传我们接下来看看是如何优雅地实现这个难顶的功能需求。
2.实现思路
所谓”优雅“,就是多写一行代码都算我输…..,所以在接口controller
层返回之前一个一个地进行脱敏操作是不可取的,重复的工作量太多。思来想去,肯定是需要通过切面思想去解决,也就是通过对接口返回的VO类
需要脱敏的字段使用注解打上标识,然后切面统一逻辑处理,这时候想到之前总结的接口响应结果结构统一封装返回:@ControllerAdvice
, 不清楚的可跳转:《Spring Boot如何优雅实现结果统一封装和异常统一处理》自行查看,但我们发现使用@ControllerAdvice
意味着每次调接口都需要我们自己去反射类获取注解判断字段是否需要脱敏,当返回对象比较复杂,需要递归去反射,性能一下子就会降低。反射这个东西确实是框架的灵魂,但是在业务接口中用多了也的确对性能有一定影响,所以再三斟酌于是换种了中思路,我们想到了平时使用的@JsonFormat
,跟我们现在的需求功能场景很类似,通过自定义注解跟字段解析器,对字段进行自定义解析。
1 | ini复制代码 @JsonFormat(pattern = "yyyy-MM-dd") |
这样就可以把字段recycleDate
转换为日期格式,不需要我们单独代码处理,这就是优雅!!!按照这个思路,我们首先需要定义一个注解进行脱敏字段标注:
1 | less复制代码@Retention(RetentionPolicy.RUNTIME) |
这个注解很简单,只有一个默认属性value()
指定字段的脱敏枚举类型MaskEnum
@Retention(RetentionPolicy.RUNTIME):
运行时生效。
@Target(ElementType.FIELD):
可用在字段上。
@JacksonAnnotationsInside:
此注解可以点进去看一下是一个元注解,主要是用户打包其他注解一起使用。
@JsonSerialize(using = MaskSerializer.class)
:该注解的作用就是可自定义序列化,可以用在注解上,方法上,字段上,类上,运行时生效等等,根据提供的序列化类里面的重写方法实现自定义序列化。关于MaskSerializer
就是我们自定义序列化实现脱敏的核心实现所在,后面会详细分析。从注解定义可以,我们给脱敏字段使用注解打标识时,需要指定该字段脱敏类型是什么,所以接下来我们还需要定义脱敏类型枚举类,我们系统大概分为两类:系统字段如(姓名,手机号,身份证号…..等等),自定义字段(婚姻状况,月供金额这些额外信息,会存储在数据库一个JSON字段里)。
1 | arduino复制代码public enum MaskEnum { |
注解和脱敏类型我们都定义好,看样子万事俱备只欠东风啦,我们只需要接下来自定义实现序列化解析器完成脱敏即可,但是你可能忘了文章开头一直强调的:动态灵活,我们的脱敏配置信息不是固定的,而是动态配置保存的,这就意味着一个接口的某个字段上一次调用还需要脱敏,紧接着脱敏配置被改了,再调接口该字段就不再需要脱敏,要求我们做到动态的同时还要保证实时性。这如同需要判断功能菜单权限那样通过切面实现判断是否需要脱敏,将脱敏配置信息上下文贯穿整次请求,这里我们在登录认证的过滤器中实现,因为脱敏配置涉及到角色、组织架构,自然是要登录之后才能进行是否需要脱敏判断。
1 | less复制代码@Component |
可以看到我们在过滤器filter中等登录验证校验通过之后,再进行脱敏配置的上下文设置,这里我们是直接在缓存redis中获取登录用户对应公司的脱敏信息配置,脱敏配置在我们SaaS系统是以公司维度配置的,也就是配置了脱敏信息那么对这个公司整体用户都有效果,而且我们在配置脱敏信息保存落库的同时也会同步保存redis,我们每次调接口都需要设置脱敏信息上下文,如果这些信息都从数据库获取(由于之前的设计脱敏配置信息要查3张表…),性能自然是吃不消的,所以我们需要从缓存redis中取脱敏配置,当然上面的实现不太完善,因为直接从redis中取有可能缓存缺失这时候就拿不到脱敏配置了,所以我们需要有一个兜底实现,在缓存中获取不到再去数据库中查询,取到之后设置上下文的同时更新缓存redis,这样下次我们再从redis中就能获取到了,这个兜底实现很简单,可自行实现哦,接下来看看定义的缓存脱敏配置信息:
1 | csharp复制代码public class MaskContextHolder { |
1 | swift复制代码@Data |
脱敏规则:DesensitizationRuleCreate
:
1 | less复制代码@ApiModel(description = "脱敏规则") |
完成脱敏信息上下文设置之后,接下来就是真正自定义实现序列化解析器完成脱敏的时刻啦,话不多说,先看看实现类:
1 | ini复制代码public class MaskSerializer<T> extends JsonSerializer<T> implements ContextualSerializer{ |
JsonSerializer
可以实现的附加接口以获取回调,该回调可用于创建序列化程序的上下文实例以用于处理支持类型的属性。这对于可以通过注释配置的序列化程序很有用,或者应该根据正在序列化的属性类型而具有不同的行为
#createContextual()
可以获得字段的类型以及注解,该方法只会在第一次序列化字段时调用(因为字段的上下文信息在运行期不会改变),所以不用担心影响性能。
最后来看看脱敏工具类实现,流行的Hutool
工具包实现了脱敏,先引入依赖:
1 | xml复制代码<dependency> |
现阶段最新版本的Hutool支持的脱敏数据类型如下,基本覆盖了常见的敏感信息:用户id、中文姓名、身份证号、座机号、手机号、地址、电子邮件、密码 、中国大陆车牌、银行卡
注意 :Hutool 脱敏是通过*来代替敏感信息的,具体实现是在StrUtil.hide方法中,如果我们想要自定义隐藏符号,则可以把Hutool的源码拷出来,重新实现即可。
但是根据上文介绍我们脱敏规则是自定义的,所以我们需要得自己实现一个脱敏工具类MaskUtils
:
1 | ini复制代码@Slf4j |
使用示例:对某个接口的VO类字段进行脱敏打上注解:
1 | scala复制代码@Data |
掉接口的响应结果如下:
1 | json复制代码{ |
3.总结
以上全部就是本期关于数据脱敏知识点的总结介绍啦。 首先介绍了数据脱敏需求的背景、概念和重要性,紧接着我们逐步探讨实现方案,权衡利弊了相关实现选择,最终选择Spring Boot
的自带的jackson
自定义序列化实现,它的实现原来其实就是在json进行序列化渲染给前端时,进行脱敏,这样可以有效降低性能损耗,并且也不会侵入系统业务层逻辑这样可以保证我们的业务逻辑不会因为数据脱敏出现逻辑错误。与此同时也强调了动态灵活可配置的脱敏信息配置,我们通过拦截器实现脱敏信息上下文设置,在上面思路我们进行代码实现剖析和实操,借助于Hutool
工具类提供的脱敏功能,完美实现了字段脱敏。
本文转载自: 掘金