基于Hutool优雅实现数据脱敏

1,701 阅读4分钟

一、简介

在掘金上看到一些关于数据脱敏的文章,也有使用Hutool编写的数据脱敏方案。但在部分处理细节上可能不够完善,因此,我想通过这篇文章分享如何优雅地使用Hutool来实现更加灵活、细致的数据脱敏处理。

二、介绍hutool的脱敏模块

官网介绍信息脱敏工具-DesensitizedUtil

在数据处理或清洗中,可能涉及到很多隐私信息的脱敏工作,因此Hutool针对常用的信息封装了一些脱敏方法。

现阶段支持的脱敏数据类型包括:

  1. 用户id
  2. 中文姓名
  3. 身份证号
  4. 座机号
  5. 手机号
  6. 地址
  7. 电子邮件
  8. 密码
  9. 中国大陆车牌,包含普通车辆、新能源车辆
  10. 银行卡

三、具体编写代码

提供一个参考的目录结构 :

image.png

1. 枚举类定义

在传统的枚举类编写中,我们通常会采用如下的写法:

public enum DesensitizationTypeEnum {
    CUSTOMIZE_RULE, // 自定义脱敏
    CHINESE_NAME,   // 中文名脱敏
    ID_CARD,        // 身份证脱敏
    // ...
}

然而,使用这种传统的枚举写法时,可能会导致在实际使用时不得不通过判断(如 switch-case)的方式来处理具体的逻辑。例如,在进行序列化时,我们可能会写出如下代码:

public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    switch (type) {
        case CUSTOMIZE_RULE:
            jsonGenerator.writeString(CharSequenceUtil.hide(str, startInclude, endExclude));
            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;
        // ...
        default:
            // 处理默认情况
    }
}

这种方式虽然可以实现功能,但存在以下问题:

  1. 代码冗余:每次添加新类型时都需要修改 switch-case,增加了维护成本。
  2. 不够优雅:将逻辑与枚举值分离,不符合面向对象编程的设计原则。

为了优化这一点,我们可以通过改造枚举类,将具体的逻辑直接与枚举值关联。如下所示:

@AllArgsConstructor
@Getter
public enum DesensitizationTypeEnum {

    /**
     * 自定义
     */
    CUSTOMIZE_RULE(str-> str),

    /**
     *  中文名
     */
     CHINESE_NAME(str->DesensitizedUtil.chineseName(String.valueOf(str))),
    /**
     * 身份证号
     */
     ID_CARD(str->DesensitizedUtil.idCardNum(String.valueOf(str), 1, 2)),

    /**
     *  座机号
     */
     FIXED_PHONE(str->DesensitizedUtil.fixedPhone(String.valueOf(str))),

    /**
     * 手机号
     */
     MOBILE_PHONE(str->DesensitizedUtil.fixedPhone(String.valueOf(str))),
    /**
     * 地址
     */
     ADDRESS(str-> DesensitizedUtil.address(String.valueOf(str),8)),

    /**
     * 电子邮件
     */
     EMAIL(str->DesensitizedUtil.email(String.valueOf(str))),

    /**
     * 密码
     */
     PASSWORD(str->DesensitizedUtil.password(String.valueOf(str))),
    /**
     * 中国大陆车牌,包含普通车辆、新能源车辆
     */
     CAR_LICENSE(str->DesensitizedUtil.carLicense(String.valueOf(str))),

    /**
     * 银行卡
     */
    BANK_CARD(str->DesensitizedUtil.bankCard(String.valueOf(str)));

    private final Function<String,String> serialize;

}

通过这种方式:

  1. 逻辑与枚举结合:每个枚举值都直接包含了相应的处理逻辑,避免了冗长的 switch-case
  2. 扩展性强:新增类型只需添加新的枚举项,而无需修改其他代码。
  3. 代码简洁:代码更简洁,易读性更好,符合开闭原则(Open-Closed Principle)。

在数据脱敏的实际应用中,大多数脱敏操作只需要简单地处理字符串,而无需太多的入参。例如,处理身份证号、手机号、邮箱等时,只需使用预定义的规则即可。但对于某些特殊情况,如自定义脱敏(CUSTOMIZE_RULE),可能需要指定更细粒度的脱敏方式(如指定位置1-2脱敏),这种情况很好,我们就在 自定义Jackson中特殊实现。

2. 定义 Desensitization 注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerialize.class)
public @interface Desensitization {
    /**
     * 脱敏数据类型,在MY_RULE的时候,startInclude和endExclude生效
     */
    DesensitizationTypeEnum type();

    /**
     * 脱敏开始位置(包含)
     */
    int startInclude() default 0;

    /**
     * 脱敏结束位置(不包含)
     */
    int endExclude() default 0;
}

3. 自定义Jackson序列化类

@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 {
        // 展示给管理员的数据不脱敏
        if (SecurityUtil.isAdmin()) {
            jsonGenerator.writeString(str);
            return;
        }
        // 不为管理员则数据脱敏
        if (type.equals(DesensitizationTypeEnum.CUSTOMIZE_RULE)) {
            jsonGenerator.writeString(CharSequenceUtil.hide(type.getSerialize().apply(str), startInclude, endExclude));
        } else {
            jsonGenerator.writeString(type.getSerialize().apply(str));
        }
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            // 检查数据类型是否为String类型
            if (beanProperty.getType().getRawClass() == String.class) {
                // 获取定义的注解
                Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
                if (desensitization != null) {
                    // 如果注解不为null,则创建并返回DesensitizationSerialize实例
                    return new DesensitizationSerialize(desensitization.type(),
                            desensitization.startInclude(), desensitization.endExclude());
                }
            }
            // 返回默认的值序列化器
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        // 如果BeanProperty为null,则返回空值序列化器
        return serializerProvider.findNullValueSerializer(null);
    }
}

4. 实际使用

4.1 数据脱敏测试

实体类中进行手机号、邮箱脱敏

@Schema(description ="当前登录用户视图对象")
@Data
public class UserInfoVO {

    @Schema(description = "手机号")
    @Desensitization(type = DesensitizationTypeEnum.MOBILE_PHONE)
    private String phoneNumber;

    @Schema(description = "邮箱")
    @Desensitization(type = DesensitizationTypeEnum.EMAIL)
    private String email;
    
    // ... 其他字段
}

4.2 数据展示

{
    email: "3********@qq.com",
    phonenumber: "1822*****45",
    ...
}

代码比较简单 : 源码