仅供自学回顾使用,请支持javaGuide原版书籍。
1.敏感词过滤方案
敏感词过滤使用比较多的是 Trie 树算法及其变体。
Trie 树 也称为字典树、单词查找树,哈希树的一种变种,通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。像浏览器搜索的关键词提示就可以基于 Trie 树来做的。
假如我们的敏感词库中有以下敏感词:
- 高清视频
- 高清 CV
- 东京冷
- 东京热
我们构造出来的敏感词 Trie 树就是下面这样的:
笑死,Guide哥也是很有生活了……
可以看出, Trie 树的核心原理其实很简单,就是通过公共前缀来提高字符串匹配效率。
Trie 树是一种利用空间换时间的数据结构,占用的内存会比较大。也正是因为这个原因,实际工程项目中都是使用的改进版 Trie 树例如双数组 Trie 树(Double-Array Trie,DAT)。DAT 的内存占用极低,可以达到 Trie 树内存的 1%左右。
AC自动机是一种建立在 Trie 树上的一种改进算法,是一种多模式匹配算法。
AC 自动机算法使用 Trie 树来存放模式串的前缀,通过失败匹配指针(失配指针)来处理匹配失败的跳转。
2.数据脱敏方案
数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。
下面是几种常见的脱敏规则:
替换(常用⭐)
:将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(*)或其他字符。删除
:将敏感数据中的部分内容随机删除。比如,将电话号码的随机 3 位数字进行删除。重排
:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。加噪
:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。加密(常用⭐)
:使用加密算法将敏感数据转换为密文。例如,将银行卡号用 MD5 或 SHA-256 等哈希函数进行散列。
2.1.Hutool
Hutool 一个 Java 基础工具类,提供以下组件:
模块 | 介绍 |
---|---|
hutool-core | 核心模块,提供Bean操作、日期处理、各类工具类等基础功能 |
hutool-db | 封装JDBC,简化数据库操作,基于ActiveRecord模式 |
hutool-http | 基于HttpUrlConnection封装的Http客户端,方便进行HTTP请求 |
hutool-json | 提供JSON数据的解析和生成功能 |
hutool-log | 自动识别日志实现,简化日志记录操作 |
hutool-crypto | 提供对称、非对称和摘要算法的加密解密功能 |
hutool-io | 输入输出流、文件操作等相关工具类 |
hutool-extra | 扩展模块,封装了模板引擎、邮件、二维码等常用功能 |
hutool-poi | 针对POI的封装,方便操作Excel和Word文档 |
hutool-socket | 基于Java NIO和AIO的Socket封装,用于网络通信 |
hutool-jwt | JSON Web Token封装实现,用于身份验证和授权 |
可以根据需求对每个模块单独引入,也可以通过引入hutool-all
方式引入所有模块,本文所使用的数据脱敏工具就是在 hutool.core 模块。
现阶段最新版本的 Hutool
支持的脱敏数据类型如下,基本覆盖了常见的敏感信息。
- 用户 id
- 中文姓名
- 身份证号
- 座机号
- 手机号
- 地址
- 电子邮件
- 密码
- 中国大陆车牌
- 银行卡
这里以手机号、银行卡号、身份证号、密码信息的脱敏为例,下面是对应的测试代码。
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)); //输出:****************
}
}
2.1.1.配合jackson通过注解方式实现脱敏
如果前端需要显示数据数据的地方比较多,我们不可能在每个地方都调用一个工具类,这样就显得代码太冗余了,那我们如何通过注解的方式优雅的完成数据脱敏呢?
如果项目是基于 Spring Boot 的 web 项目,则可以利用 Spring Boot 自带的 jackson 自定义序列化实现。它的实现原理其实就是在 json 进行序列化渲染给前端时,进行脱敏。
2.1.1.1.脱敏策略的枚举
/**
* @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
}
2.1.1.2.定义一个用于脱敏的 Desensitization 注解
- @Retention (RetentionPolicy.RUNTIME):运行时生效。
- @Target (ElementType.FIELD):可用在字段上。
- @JacksonAnnotationsInside:此注解可以点进去看一下是一个元注解,主要是用户打包其他注解一起使用。\
- @JsonSerialize:上面说到过,该注解的作用就是可自定义序列化,可以用在注解上,方法上,字段上,类上,运行时生效等等,根据提供的序列化类里面的重写方法实现自定义序列化。
/**
* @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 的时候,开始位置和结束位置才生效。
2.1.1.3.创建自定的序列化类
这一步是我们实现数据脱敏的关键。自定义序列化类继承 JsonSerializer,实现 ContextualSerializer 接口,并重写两个方法。
/**
* @author
* @description: 自定义序列化类
*/
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizationSerialize extends JsonSerializer<String> 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);
}
}
2.1.1.4.测试
首先定义一个要测试的 pojo,对应的字段加入要脱敏的策略。
/**
*
* @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
@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;
}
}
2.2.MyBatis-Flex
类似于 MybatisPlus,MyBatis-Flex 也是一个 MyBatis 增强框架。MyBatis-Flex 同样提供了数据脱敏功能,并且是可以免费使用的。
2.2.1.内置脱敏规则
MyBatis-Flex 提供了 @ColumnMask() 注解,以及内置的 9 种脱敏规则,开箱即用:
/**
* 内置的数据脱敏方式
*/
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";
//...
}
使用示例:
@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;
}
2.2.2.自定义脱敏规则
1、通过 MaskManager 注册新的脱敏规则:
MaskManager.registerMaskProcessor("自定义规则名称"
, data -> {
return data;
})
2、使用自定义的脱敏规则@Table(“tb_account”)
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
@ColumnMask("自定义规则名称")
private String userName;
}
并且,对于需要跳过脱密处理的场景,例如进入编辑页面编辑用户数据,MyBatis-Flex 也提供了对应的支持:MaskManager#execWithoutMask
。
该方法使用了模版方法设计模式,保障跳过脱敏处理并执行相关逻辑后自动恢复脱敏处理。
实现如下:
public static <T> T execWithoutMask(Supplier<T> supplier) {
try {
skipMask();
return supplier.get();
} finally {
restoreMask();
}
}
MaskManager 的skipMask和restoreMask方法一般配套使用,推荐try{...}finally{...}
模式。
2.3.FastJson
FastJSON 实现数据脱敏的方式主要有两种:
- 基于注解 @JSONField 实现:需要自定义一个用于脱敏的序列化的类,然后在需要脱敏的字段上通过 @JSONField 中的 serializeUsing 指定为我们自定义的序列化类型即可。
- 基于序列化过滤器:需要实现 ValueFilter 接口,重写 process 方法完成自定义脱敏,然后在 JSON 转换时使用自定义的转换策略。
最后,除了以上,javaGuide还写了Apache ShardingSphere、Mybatis-Mate两种方案,但个人觉得使用不多,所以没有加上。