系统设计面试总结:6、敏感词过滤方案:Trie字典树、DAT、AC自动机;数据脱敏方案:Hutool(配合jackson通过注解实现脱敏)、FastJson、MyBatis-Flex(自定义代码实现)

仅供自学回顾使用,请支持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两种方案,但个人觉得使用不多,所以没有加上。

猜你喜欢

转载自blog.csdn.net/zhiaidaidai/article/details/146212294
今日推荐