From 676e16802e52a849621e6ade77c1fd80e7b69f67 Mon Sep 17 00:00:00 2001 From: Guide Date: Sat, 26 Aug 2023 19:36:50 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20add]=E8=A1=A5=E5=85=85=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=84=B1=E6=95=8F=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/system-design/schedule-task.md | 106 ++-- .../security/data-desensitization.md | 473 +++++++++++++++++- 2 files changed, 549 insertions(+), 30 deletions(-) diff --git a/docs/system-design/schedule-task.md b/docs/system-design/schedule-task.md index e9fc2ef1..c00fdef7 100644 --- a/docs/system-design/schedule-task.md +++ b/docs/system-design/schedule-task.md @@ -15,16 +15,21 @@ head: 我们来看一下几个非常常见的业务场景: -1. 某系统凌晨要进行数据备份。 +1. 某系统凌晨 1 点要进行数据备份。 2. 某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。 3. 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。 4. 某博客平台,支持定时发送文章。 5. 某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。 6. ...... -这些场景往往都要求我们在某个特定的时间去做某个事情。 +这些场景往往都要求我们在某个特定的时间去做某个事情,也就是定时或者延时去做某个事情。 -## 单机定时任务技术选型 +- 定时任务:在指定时间点执行特定的任务,例如每天早上 8 点,每周一下午 3 点等。定时任务可以用来做一些周期性的工作,如数据备份,日志清理,报表生成等。 +- 延时任务:一定的延迟时间后执行特定的任务,例如 10 分钟后,3 小时后等。延时任务可以用来做一些异步的工作,如订单取消,推送通知,红包撤回等。 + +尽管二者的适用场景有所区别,但它们的核心思想都是将任务的执行时间安排在未来的某个点上,以达到预期的调度效果。 + +## 单机定时任务 ### Timer @@ -113,6 +118,16 @@ executor.shutdown(); 不论是使用 `Timer` 还是 `ScheduledExecutorService` 都无法使用 Cron 表达式指定任务执行的具体时间。 +### DelayQueue + +`DelayQueue` 是 JUC 包(`java.util.concurrent)`为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 `BlockingQueue` 的一种,底层是一个基于 `PriorityQueue` 实现的一个无界队列,是线程安全的。关于`PriorityQueue`可以参考笔者编写的这篇文章:[PriorityQueue 源码分析](https://javaguide.cn/java/collection/priorityqueue-source-code.html) 。 + +![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) + +`DelayQueue` 和 `Timer/TimerTask` 都可以用于实现定时任务调度,但是它们的实现方式不同。`DelayQueue` 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 `Timer/TimerTask` 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,`DelayQueue` 还支持动态添加和移除任务,而 `Timer/TimerTask` 只能在创建时指定任务。 + +关于 `DelayQueue` 的详细介绍,请参考我写的这篇文章:[`DelayQueue` 源码分析](https://javaguide.cn/java/collection/delayqueue-source-code.html)。 + ### Spring Task 我们直接通过 Spring 提供的 `@Scheduled` 注解即可定义定时任务,非常方便! @@ -153,7 +168,7 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 ![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/one-layers-of-time-wheel.png) -那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入一叫做 **圈数/轮数** 的概念,也就是说这个任务还是放在下标为 3 的时间格中, 不过它的圈数为 2 。 +那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入一叫做 **圈数/轮数** 的概念,也就是说这个任务还是放在下标为 1 的时间格中, 不过它的圈数为 2 。 除了增加圈数这种方法之外,还有一种 **多层次时间轮** (类似手表),Kafka 采用的就是这种方案。 @@ -171,13 +186,35 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 **时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是 0(1)。** -## 分布式定时任务技术选型 +## 分布式定时任务 -上面提到的一些定时任务的解决方案都是在单机下执行的,适用于比较简单的定时任务场景比如每天凌晨备份一次数据。 +### Redis + +Redis 是可以用来做延时任务的,基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +这部分内容的详细介绍我放在了[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html)中,有需要的同学可以进入星球后阅读学习。篇幅太多,这里就不重复分享了。 + +![《后端面试高频系统设计&场景题》](https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png) + +### MQ + +大部分消息队列,例如 RocketMQ、RabbitMQ,都支持定时/延时消息。定时消息和延时消息本质其实是相同的,都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。 + +不过,在使用 MQ 定时消息之前一定要看清楚其使用限制,以免不适合项目需求,例如 RocketMQ 定时时长最大值默认为 24 小时且不支持自定义修改、只支持 18 个 Level 的延时并不支持任意时间。 + +**优缺点总结:** + +- **优点**:可以与 Spring 集成、支持分布式、支持集群、性能不错 +- **缺点**:功能性较差、不灵活、需要保障消息可靠性 + +## 分布式任务调度框架 如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。 -通常情况下,一个定时任务的执行往往涉及到下面这些角色: +通常情况下,一个分布式定时任务的执行往往涉及到下面这些角色: - **任务**:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。 - **调度器**:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。 @@ -185,9 +222,9 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 ### Quartz -一个很火的开源任务调度框架,完全由Java写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的`elastic-job`就是基于Quartz二次开发之后的分布式调度解决方案。 +一个很火的开源任务调度框架,完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的`elastic-job`就是基于 Quartz 二次开发之后的分布式调度解决方案。 -使用 Quartz 可以很方便地与 Spring集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。 +使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。 并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 [quartzui](https://github.com/zhaopeiym/quartzui) 这个开源项目来解决这个问题。 @@ -195,12 +232,12 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 **优缺点总结:** -- 优点:可以与 Spring集成,并且支持动态添加任务和集群。 -- 缺点:分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相比于其他同类型框架来说) +- 优点:可以与 Spring 集成,并且支持动态添加任务和集群。 +- 缺点:分布式支持不友好,不支持任务可视化管理、使用麻烦(相比于其他同类型框架来说) ### Elastic-Job -ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 +ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 ElasticJob-Lite 和 ElasticJob-Cloud 两者的对比如下: @@ -219,7 +256,7 @@ ElasticJob-Lite 的架构设计如下图所示: ![ElasticJob-Lite 的架构设计](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/elasticjob-lite-architecture-design.png) -从上图可以看出,Elastic-Job没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。 +从上图可以看出,Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。 Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。 @@ -243,7 +280,7 @@ public class TestJob implements SimpleJob { **优缺点总结:** -- 优点:可以与 Spring集成、支持分布式、支持集群、性能不错 +- 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错、支持任务可视化管理 - 缺点:依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高) ### XXL-JOB @@ -254,14 +291,14 @@ public class TestJob implements SimpleJob { 根据 `XXL-JOB` 官网介绍,其解决了很多 Quartz 的不足。 -> Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: +> Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: > -> - 问题一:调用API的的方式操作任务,不人性化; -> - 问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。 -> - 问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; -> - 问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 +> - 问题一:调用 API 的的方式操作任务,不人性化; +> - 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。 +> - 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; +> - 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 > -> XXL-JOB弥补了quartz的上述不足之处。 +> XXL-JOB 弥补了 quartz 的上述不足之处。 `XXL-JOB` 的架构设计如下图所示: @@ -269,7 +306,7 @@ public class TestJob implements SimpleJob { 从上图可以看出,`XXL-JOB` 由 **调度中心** 和 **执行器** 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。 -不同于 Elastic-Job的去中心化设计, `XXL-JOB` 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。 +不同于 Elastic-Job 的去中心化设计, `XXL-JOB` 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。 和 `Quzrtz` 类似 `XXL-JOB` 也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。 @@ -307,7 +344,7 @@ public ReturnT myAnnotationJobHandler(String param) throws Exception { **优缺点总结:** -- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、内置了 UI 管理控制台。 +- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。 - 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:[xxl-job issue277](https://github.com/xuxueli/xxl-job/issues/277))。 ### PowerJob @@ -320,17 +357,28 @@ public ReturnT myAnnotationJobHandler(String param) throws Exception { 由于 SchedulerX 属于人民币产品,我这里就不过多介绍。PowerJob 官方也对比过其和 QuartZ、XXL-JOB 以及 SchedulerX。 -![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/quartz-xxljob-schedulerx2.0-powerjob-comparison.png) +| | QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | +| -------------- | ------------------------------------------ | ---------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ | +| 定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | **CRON、固定频率、固定延迟、OpenAPI** | +| 任务类型 | 内置Java | 内置Java、GLUE Java、Shell、Python等脚本 | 内置Java、外置Java(FatJar)、Shell、Python等脚本 | **内置Java、外置Java(容器)、Shell、Python等脚本** | +| 分布式计算 | 无 | 静态分片 | MapReduce动态分片 | **MapReduce动态分片** | +| 在线任务治理 | 不支持 | 支持 | 支持 | **支持** | +| 日志白屏化 | 不支持 | 支持 | 不支持 | **支持** | +| 调度方式及性能 | 基于数据库锁,有性能瓶颈 | 基于数据库锁,有性能瓶颈 | 不详 | **无锁化设计,性能强劲无上限** | +| 报警监控 | 无 | 邮件 | 短信 | **WebHook、邮件、钉钉与自定义扩展** | +| 系统依赖 | JDBC支持的关系型数据库(MySQL、Oracle...) | MySQL | 人民币 | **任意Spring Data Jpa支持的关系型数据库(MySQL、Oracle...)** | +| DAG工作流 | 不支持 | 不支持 | 支持 | **支持** | -## 总结 +## 定时任务方案总结 -这篇文章中,我主要介绍了: +单机定时任务的常见解决方案有 `Timer`、`ScheduledExecutorService`、`DelayQueue`、Spring Task 和时间轮,其中最常用也是比较推荐使用的是时间轮。另外,这几种单机定时任务解决方案同样可以实现延时任务。 -- **定时任务的相关概念**:为什么需要定时任务、定时任务中的核心角色、分布式定时任务。 -- **定时任务的技术选型**:XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。 +Redis 和 MQ 虽然可以实现分布式定时任务,但这两者本身不是专门用来做分布式定时任务的,它们并不提供较为完整和强大的分布式定时任务的功能。而且,两者不太适合执行周期性的定时任务,因为它们只能保证消息被消费一次,而不能保证消息被消费多次。因此,它们更适合执行一次性的延时任务,例如订单取消、红包撤回。实际项目中,MQ 延时任务用的更多一些,可以降低业务之间的耦合度。 + +Quartz、Elastic-Job、XXL-JOB 和 PowerJob 这几个是专门用来做分布式调度的框架,提供的分布式定时任务的功能更为完善和强大,更加适合执行周期性的定时任务。除了 Quartz 之外,另外三者都是支持任务可视化管理的。 + +XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。 这篇文章并没有介绍到实际使用,但是,并不代表实际使用不重要。我在写这篇文章之前,已经动手写过相应的 Demo。像 Quartz,我在大学那会就用过。不过,当时用的是 Spring 。为了能够更好地体验,我自己又在 Spring Boot 上实际体验了一下。如果你并没有实际使用某个框架,就直接说它并不好用的话,是站不住脚的。 -最后,这篇文章要感谢艿艿的帮助,写这篇文章的时候向艿艿询问过一些问题。推荐一篇艿艿写的偏实战类型的硬核文章:[《Spring Job?Quartz?XXL-Job?年轻人才做选择,艿艿全莽~》](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247490679&idx=1&sn=25374dbdcca95311d41be5d7b7db454d&chksm=fa4963c6cd3eead055bb9cd10cca13224bb35d0f7373a27aa22a55495f71e24b8273a7603314&scene=27#wechat_redirect) 。 - \ No newline at end of file diff --git a/docs/system-design/security/data-desensitization.md b/docs/system-design/security/data-desensitization.md index 6f23dc91..7261baab 100644 --- a/docs/system-design/security/data-desensitization.md +++ b/docs/system-design/security/data-desensitization.md @@ -5,6 +5,477 @@ tag: - 安全 --- -数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。 +> 本文转载完善自[Hutool:一行代码搞定数据脱敏 - 京东云开发者](https://mp.weixin.qq.com/s/1qFWczesU50ndPPLtABHFg)。 + +## 什么是数据脱敏 + +### 数据脱敏的定义 + +数据脱敏百度百科中是这样定义的: + +> 数据脱敏,指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。这样就可以在开发、测试和其它非生产环境以及外包环境中安全地使用脱敏后的真实数据集。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。是数据库安全技术之一。 + +总的来说,数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。 + +在数据脱敏过程中,通常会采用不同的算法和技术,以根据不同的需求和场景对数据进行处理。例如,对于身份证号码,可以使用掩码算法(masking)将前几位数字保留,其他位用 “X” 或 "\*" 代替;对于姓名,可以使用伪造(pseudonymization)算法,将真实姓名替换成随机生成的假名。 + +### 常用脱敏规则 + +常用脱敏规则是为了保护敏感数据的安全性,在处理和存储敏感数据时对其进行变换或修改。 + +下面是几种常见的脱敏规则: + +- 替换(常用):将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(\*)或其他字符。 +- 删除:将敏感数据中的部分内容随机删除。比如,将电话号码的随机 3 位数字进行删除。 +- 重排:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。 +- 加噪:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。 +- 加密(常用):使用加密算法将敏感数据转换为密文。例如,将银行卡号用 MD5 或 SHA-256 等哈希函数进行散列。常见加密算法总结可以参考这篇文章: 。 +- ...... + +## 常用脱敏工具 + +### Hutool + +Hutool 一个 Java 基础工具类,对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,组成各种 Util 工具类,同时提供以下组件: + +| 模块 | 介绍 | +| :----------------: | :----------------------------------------------------------: | +| hutool-aop | JDK 动态代理封装,提供非 IOC 下的切面支持 | +| hutool-bloomFilter | 布隆过滤,提供一些 Hash 算法的布隆过滤 | +| hutool-cache | 简单缓存实现 | +| hutool-core | 核心,包括 Bean 操作、日期、各种 Util 等 | +| hutool-cron | 定时任务模块,提供类 Crontab 表达式的定时任务 | +| hutool-crypto | 加密解密模块,提供对称、非对称和摘要算法封装 | +| hutool-db | JDBC 封装后的数据操作,基于 ActiveRecord 思想 | +| hutool-dfa | 基于 DFA 模型的多关键字查找 | +| hutool-extra | 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等) | +| hutool-http | 基于 HttpUrlConnection 的 Http 客户端封装 | +| hutool-log | 自动识别日志实现的日志门面 | +| hutool-script | 脚本执行封装,例如 Javascript | +| hutool-setting | 功能更强大的 Setting 配置文件和 Properties 封装 | +| hutool-system | 系统参数调用封装(JVM 信息等) | +| hutool-json | JSON 实现 | +| hutool-captcha | 图片验证码实现 | +| hutool-poi | 针对 POI 中 Excel 和 Word 的封装 | +| hutool-socket | 基于 Java 的 NIO 和 AIO 的 Socket 封装 | +| hutool-jwt | JSON Web Token (JWT) 封装实现 | + +可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块,本文所使用的数据脱敏工具就是在 `hutool.core` 模块。 + +现阶段最新版本的 Hutool 支持的脱敏数据类型如下,基本覆盖了常见的敏感信息。 + +1. 用户 id +2. 中文姓名 +3. 身份证号 +4. 座机号 +5. 手机号 +6. 地址 +7. 电子邮件 +8. 密码 +9. 中国大陆车牌,包含普通车辆、新能源车辆 +10. 银行卡 + +#### 一行代码实现脱敏 + +Hutool 提供的脱敏方法如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/2023-08-01-10-2119fnVCIDozqHgRGx.png) + +注意:Hutool 脱敏是通过 \* 来代替敏感信息的,具体实现是在 StrUtil.hide 方法中,如果我们想要自定义隐藏符号,则可以把 Hutool 的源码拷出来,重新实现即可。 + +这里以手机号、银行卡号、身份证号、密码信息的脱敏为例,下面是对应的测试代码。 + +```java +import cn.hutool.core.util.DesensitizedUtil; +import org.junit.Test; +import org.springframework.boot.test.context.Spring BootTest; + +/** + * + * @description: Hutool实现数据脱敏 + */ +@Spring BootTest +public class HuToolDesensitizationTest { + + @Test + public void testPhoneDesensitization(){ + String phone="13723231234"; + System.out.println(DesensitizedUtil.mobilePhone(phone)); //输出:137****1234 + } + @Test + public void testBankCardDesensitization(){ + String bankCard="6217000130008255666"; + System.out.println(DesensitizedUtil.bankCard(bankCard)); //输出:6217 **** **** *** 5666 + } + + @Test + public void testIdCardNumDesensitization(){ + String idCardNum="411021199901102321"; + //只显示前4位和后2位 + System.out.println(DesensitizedUtil.idCardNum(idCardNum,4,2)); //输出:4110************21 + } + @Test + public void testPasswordDesensitization(){ + String password="www.jd.com_35711"; + System.out.println(DesensitizedUtil.password(password)); //输出:**************** + } +} +``` + +以上就是使用 Hutool 封装好的工具类实现数据脱敏。 + +#### 配合 JackSon 通过注解方式实现脱敏 + +现在有了数据脱敏工具类,如果前端需要显示数据数据的地方比较多,我们不可能在每个地方都调用一个工具类,这样就显得代码太冗余了,那我们如何通过注解的方式优雅的完成数据脱敏呢? + +如果项目是基于 Spring Boot 的 web 项目,则可以利用 Spring Boot 自带的 jackson 自定义序列化实现。它的实现原来其实就是在 json 进行序列化渲染给前端时,进行脱敏。 + +**第一步:脱敏策略的枚举。** + +```java +/** + * @author + * @description:脱敏策略枚举 + */ +public enum DesensitizationTypeEnum { + //自定义 + MY_RULE, + //用户id + USER_ID, + //中文名 + CHINESE_NAME, + //身份证号 + ID_CARD, + //座机号 + FIXED_PHONE, + //手机号 + MOBILE_PHONE, + //地址 + ADDRESS, + //电子邮件 + EMAIL, + //密码 + PASSWORD, + //中国大陆车牌,包含普通车辆、新能源车辆 + CAR_LICENSE, + //银行卡 + BANK_CARD +} +``` + +上面表示支持的脱敏类型。 + +**第二步:定义一个用于脱敏的 Desensitization 注解。** + +- `@Retention (RetentionPolicy.RUNTIME)`:运行时生效。 +- `@Target (ElementType.FIELD)`:可用在字段上。 +- `@JacksonAnnotationsInside`:此注解可以点进去看一下是一个元注解,主要是用户打包其他注解一起使用。 +- `@JsonSerialize`:上面说到过,该注解的作用就是可自定义序列化,可以用在注解上,方法上,字段上,类上,运行时生效等等,根据提供的序列化类里面的重写方法实现自定义序列化。 + +```java +/** + * @author + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@JsonSerialize(using = DesensitizationSerialize.class) +public @interface Desensitization { + /** + * 脱敏数据类型,在MY_RULE的时候,startInclude和endExclude生效 + */ + DesensitizationTypeEnum type() default DesensitizationTypeEnum.MY_RULE; + + /** + * 脱敏开始位置(包含) + */ + int startInclude() default 0; + + /** + * 脱敏结束位置(不包含) + */ + int endExclude() default 0; +} +``` + +注:只有使用了自定义的脱敏枚举 `MY_RULE` 的时候,开始位置和结束位置才生效。 + +**第三步:创建自定的序列化类** + +这一步是我们实现数据脱敏的关键。自定义序列化类继承 `JsonSerializer`,实现 `ContextualSerializer` 接口,并重写两个方法。 + +```java +/** + * @author + * @description: 自定义序列化类 + */ +@AllArgsConstructor +@NoArgsConstructor +public class DesensitizationSerialize extends JsonSerializer implements ContextualSerializer { + private DesensitizationTypeEnum type; + + private Integer startInclude; + + private Integer endExclude; + + @Override + public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + switch (type) { + // 自定义类型脱敏 + case MY_RULE: + jsonGenerator.writeString(CharSequenceUtil.hide(str, startInclude, endExclude)); + break; + // userId脱敏 + case USER_ID: + jsonGenerator.writeString(String.valueOf(DesensitizedUtil.userId())); + break; + // 中文姓名脱敏 + case CHINESE_NAME: + jsonGenerator.writeString(DesensitizedUtil.chineseName(String.valueOf(str))); + break; + // 身份证脱敏 + case ID_CARD: + jsonGenerator.writeString(DesensitizedUtil.idCardNum(String.valueOf(str), 1, 2)); + break; + // 固定电话脱敏 + case FIXED_PHONE: + jsonGenerator.writeString(DesensitizedUtil.fixedPhone(String.valueOf(str))); + break; + // 手机号脱敏 + case MOBILE_PHONE: + jsonGenerator.writeString(DesensitizedUtil.mobilePhone(String.valueOf(str))); + break; + // 地址脱敏 + case ADDRESS: + jsonGenerator.writeString(DesensitizedUtil.address(String.valueOf(str), 8)); + break; + // 邮箱脱敏 + case EMAIL: + jsonGenerator.writeString(DesensitizedUtil.email(String.valueOf(str))); + break; + // 密码脱敏 + case PASSWORD: + jsonGenerator.writeString(DesensitizedUtil.password(String.valueOf(str))); + break; + // 中国车牌脱敏 + case CAR_LICENSE: + jsonGenerator.writeString(DesensitizedUtil.carLicense(String.valueOf(str))); + break; + // 银行卡脱敏 + case BANK_CARD: + jsonGenerator.writeString(DesensitizedUtil.bankCard(String.valueOf(str))); + break; + default: + } + + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { + if (beanProperty != null) { + // 判断数据类型是否为String类型 + if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { + // 获取定义的注解 + Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class); + // 为null + if (desensitization == null) { + desensitization = beanProperty.getContextAnnotation(Desensitization.class); + } + // 不为null + if (desensitization != null) { + // 创建定义的序列化类的实例并且返回,入参为注解定义的type,开始位置,结束位置。 + return new DesensitizationSerialize(desensitization.type(), desensitization.startInclude(), + desensitization.endExclude()); + } + } + + return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); + } + return serializerProvider.findNullValueSerializer(null); + } +} +``` + +经过上述三步,已经完成了通过注解实现数据脱敏了,下面我们来测试一下。 + +首先定义一个要测试的 pojo,对应的字段加入要脱敏的策略。 + +```java +/** + * + * @description: + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestPojo { + + private String userName; + + @Desensitization(type = DesensitizationTypeEnum.MOBILE_PHONE) + private String phone; + + @Desensitization(type = DesensitizationTypeEnum.PASSWORD) + private String password; + + @Desensitization(type = DesensitizationTypeEnum.MY_RULE, startInclude = 0, endExclude = 2) + private String address; +} +``` + +接下来写一个测试的 controller + +```java +@RestController +public class TestController { + + @RequestMapping("/test") + public TestPojo testDesensitization(){ + TestPojo testPojo = new TestPojo(); + testPojo.setUserName("我是用户名"); + testPojo.setAddress("地球中国-北京市通州区京东总部2号楼"); + testPojo.setPhone("13782946666"); + testPojo.setPassword("sunyangwei123123123."); + System.out.println(testPojo); + return testPojo; + } + +} +``` + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/2023-08-02-16-497DdCBy8vbf2D69g.png) + +可以看到我们成功实现了数据脱敏。 + +### Apache ShardingSphere + +ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能 。 + +Apache ShardingSphere 下面存在一个数据脱敏模块,此模块集成的常用的数据脱敏的功能。其基本原理是对用户输入的 SQL 进行解析拦截,并依靠用户的脱敏配置进行 SQL 的改写,从而实现对原文字段的加密及加密字段的解密。最终实现对用户无感的加解密存储、查询。 + +通过 Apache ShardingSphere 可以自动化&透明化数据脱敏过程,用户无需关注脱敏中间实现细节。并且,提供了多种内置、第三方(AKS)的脱敏策略,用户仅需简单配置即可使用。 + +官方文档地址: 。 + +### FastJSON + +平时开发 Web 项目的时候,除了默认的 Spring 自带的序列化工具,FastJson 也是一个很常用的 Spring Web Restful 接口序列化的工具。 + +FastJSON 实现数据脱敏的方式主要有两种: + +- 基于注解 `@JSONField` 实现:需要自定义一个用于脱敏的序列化的类,然后在需要脱敏的字段上通过 `@JSONField` 中的 `serializeUsing` 指定为我们自定义的序列化类型即可。 +- 基于序列化过滤器:需要实现 `ValueFilter` 接口,重写 `process` 方法完成自定义脱敏,然后在 JSON 转换时使用自定义的转换策略。具体实现可参考这篇文章: 。 + +### Mybatis-mate + +MybatisPlus 也提供了数据脱敏模块 mybatis-mate。mybatis-mate 为 MybatisPlus 企业级模块,使用之前需要配置授权码(付费),旨在更敏捷优雅处理数据。 + +配置内容如下所示: + +```yaml +# Mybatis Mate 配置 +mybatis-mate: + cert: + grant: jxftsdfggggx + license: GKXP9r4MCJhGID/DTGigcBcLmZjb1YZGjE4GXaAoxbtGsPC20sxpEtiUr2F7Nb1ANTUekvF6Syo6DzraA4M4oacwoLVTglzfvaEfadfsd232485eLJK1QsskrSJmreMnEaNh9lsV7Lpbxy9JeGCeM0HPEbRvq8Y+8dUt5bQYLklsa3ZIBexir+4XykZY15uqn1pYIp4pEK0+aINTa57xjJNoWuBIqm7BdFIb4l1TAcPYMTsMXhF5hfMmKD2h391HxWTshJ6jbt4YqdKD167AgeoM+B+DE1jxlLjcpskY+kFs9piOS7RCcmKBBUOgX2BD/JxhR2gQ== +``` + +具体实现可参考 baomidou 提供的如下代码: 。 + +### MyBatis-Flex + +类似于 MybatisPlus,MyBatis-Flex 也是一个 MyBatis 增强框架。MyBatis-Flex 同样提供了数据脱敏功能,并且是可以免费使用的。 + +MyBatis-Flex 提供了 `@ColumnMask()` 注解,以及内置的 9 种脱敏规则,开箱即用: + +- 用户名脱敏 +- 手机号脱敏 +- 固定电话脱敏 +- 身份证号脱敏 +- 车牌号脱敏 +- 地址脱敏 +- 邮件脱敏 +- 密码脱敏 +- 银行卡号脱敏 + +```java +/** + * 内置的数据脱敏方式 + */ +public class Masks { + /** + * 手机号脱敏 + */ + public static final String MOBILE = "mobile"; + /** + * 固定电话脱敏 + */ + public static final String FIXED_PHONE = "fixed_phone"; + /** + * 身份证号脱敏 + */ + public static final String ID_CARD_NUMBER = "id_card_number"; + /** + * 中文名脱敏 + */ + public static final String CHINESE_NAME = "chinese_name"; + /** + * 地址脱敏 + */ + public static final String ADDRESS = "address"; + /** + * 邮件脱敏 + */ + public static final String EMAIL = "email"; + /** + * 密码脱敏 + */ + public static final String PASSWORD = "password"; + /** + * 车牌号脱敏 + */ + public static final String CAR_LICENSE = "car_license"; + /** + * 银行卡号脱敏 + */ + public static final String BANK_CARD_NUMBER = "bank_card_number"; + //... +} +``` + +使用示例: + +```java +@Table("tb_account") +public class Account { + + @Id(keyType = KeyType.Auto) + private Long id; + + @ColumnMask(Masks.CHINESE_NAME) + private String userName; + + @ColumnMask(Masks.EMAIL) + private String email; + +} +``` + +如果这些内置的脱敏规则不满足你的要求的话,你还可以自定义脱敏规则。 + +## 总结 + +本文主要介绍了数据脱敏的相关内容,首先介绍了数据脱敏的概念,在此基础上介绍了常用的数据脱敏规则;随后介绍了本文的重点 Hutool 工具及其使用方法,在此基础上进行了实操,分别演示了使用 DesensitizedUtil 工具类、配合 Jackson 通过注解的方式完成数据脱敏;最后,介绍了一些常见的数据脱敏方法,并附上了对应的教程链接供大家参考,本文内容如有不当之处,还请大家批评指正。 + +## 推荐阅读 + +- [Spring Boot 日志、配置文件、接口数据如何脱敏?老鸟们都是这样玩的!](https://mp.weixin.qq.com/s/59osrnjyPJ7BV070x6ABwQ) +- [大厂也在用的6种数据脱敏方案,严防泄露数据的“内鬼”](https://mp.weixin.qq.com/s/_Dgekk1AJsIx0TTlnH6kUA) + +## 参考 + +- Hutool 工具官网: +- 聊聊如何自定义数据脱敏: +- FastJSON 实现数据脱敏: \ No newline at end of file