Bootstrap

详细分析Java中的脱敏注解(附Demo)

前言

对于隐私信息,需要做特殊处理,比如身份证或者手机号等

对于Java的相关知识推荐阅读:java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)

1. 基本知识

脱敏(Desensitization)指在保持数据结构不变的前提下,对敏感数据进行处理,使其不再具备直接识别个人身份或敏感信息的能力,从而保护用户隐私

常见的脱敏方法包括:

  • 替换(Masking):将敏感数据中的一部分或全部字符替换为特定字符,如将姓名中的一部分字符替换为星号
  • 截断(Truncation):截取敏感数据的一部分,只保留部分信息,如只保留电话号码的前几位
  • 加密(Encryption):使用算法将敏感数据转换为密文,只有经过解密才能还原为原始数据
  • 哈希(Hashing):将敏感数据通过哈希算法转换为固定长度的哈希值,不可逆转

序列化器是指在将对象转换为字节流或其他格式时,负责对对象进行序列化的组件。在脱敏处理中,序列化器可以通过自定义的逻辑对敏感数据进行处理,使其在序列化过程中不泄露隐私信息
主要将其自定义注解继承自 Jackson 库中的 JsonSerializer 类,在序列化过程中做一定的处理

2. 核心逻辑

在定义的字段中加入自定义注解类

类似如下:

@Data
public static class DesensitizeDemo {
    @ChineseNameDesensitize
    private String nickname;
}

对应注解的核心内容如下:

// 脱敏注解
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = ChineseNameDesensitization.Serializer.class)
@interface ChineseNameDesensitize {
    int prefixKeep() default 1; // 前缀保留的字符数,默认为1
    int suffixKeep() default 2; // 后缀保留的字符数,默认为2
    String replacer() default "*"; // 替换字符,默认为 "*"
}

其中涉及的改造方法也可通过某个类进行重写

// 脱敏方法
private String desensitize(String value) {
    // 实现自定义的脱敏逻辑,根据注解参数进行处理
    String prefix = value.substring(0, Math.min(prefixKeep, value.length()));
    String suffix = value.substring(Math.max(0, value.length() - suffixKeep));
    String maskedPart = StringUtils.repeat(replacer, value.length() - prefixKeep - suffixKeep);
    return prefix + maskedPart + suffix;
}

后续测试的时候直接调用即可实现脱敏数据

3. Demo

直接在Demo文件中执行,先看一个Demo的执行方式

// 导入注解相关的类
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.module.SimpleModule;

// 导入 lombok 提供的注解
import lombok.Data;

// 导入 Apache Commons Lang 库中的 StringUtils 类
import org.apache.commons.lang3.StringUtils;

// 导入 IOException 异常类
import java.io.IOException;

// 导入元注解相关的类
import java.lang.annotation.*;

// 脱敏注解
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = ChineseNameDesensitization.Serializer.class)
@interface ChineseNameDesensitize {
    int prefixKeep() default 1; // 前缀保留的字符数,默认为1
    int suffixKeep() default 2; // 后缀保留的字符数,默认为2
    String replacer() default "*"; // 替换字符,默认为 "*"
}

// 脱敏处理器
@Data
class ChineseNameDesensitization {
    public static class Serializer extends JsonSerializer<String> {
        private int prefixKeep; // 前缀保留的字符数
        private int suffixKeep; // 后缀保留的字符数
        private String replacer; // 替换字符

        // 默认构造函数
        public Serializer() {
            this.prefixKeep = 1;
            this.suffixKeep = 2;
            this.replacer = "*";
        }

        // 带参构造函数
        public Serializer(int prefixKeep, int suffixKeep, String replacer) {
            this.prefixKeep = prefixKeep;
            this.suffixKeep = suffixKeep;
            this.replacer = replacer;
        }

        // 序列化方法
        @Override
        public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            System.out.println("Value before desensitization: " + value);
            System.out.println("Prefix keep: " + prefixKeep);
            System.out.println("Suffix keep: " + suffixKeep);
            System.out.println("Replacer: " + replacer);

            if (StringUtils.isNotBlank(value)) {
                String desensitizedValue = desensitize(value); // 调用脱敏方法
                gen.writeString(desensitizedValue);
            } else {
                gen.writeString(value);
            }
        }

        // 脱敏方法
        private String desensitize(String value) {
            // 实现自定义的脱敏逻辑,根据注解参数进行处理
            String prefix = value.substring(0, Math.min(prefixKeep, value.length()));
            String suffix = value.substring(Math.max(0, value.length() - suffixKeep));
            String maskedPart = StringUtils.repeat(replacer, value.length() - prefixKeep - suffixKeep);
            return prefix + maskedPart + suffix;
        }
    }
}

// 直接执行的 Demo 类
public class test {
    public static void main(String[] args) throws IOException {
        // 创建 ObjectMapper 对象
        ObjectMapper objectMapper = new ObjectMapper();
        // 创建 SimpleModule 对象
        SimpleModule module = new SimpleModule();

        // 准备参数
        int prefixKeep = 1;
        int suffixKeep = 2;
        String replacer = "*";

        // 创建带参数的 ChineseNameDesensitization.Serializer 对象
        ChineseNameDesensitization.Serializer serializer = new ChineseNameDesensitization.Serializer(prefixKeep, suffixKeep, replacer);

        // 注册脱敏处理器并应用于注解中定义的字段
        module.addSerializer(String.class, serializer);
        objectMapper.registerModule(module);

        // 准备参数
        DesensitizeDemo demo = new DesensitizeDemo();
        demo.setNickname("码农研究僧");

        // 将对象序列化为 JSON 字符串并输出
        String json = objectMapper.writeValueAsString(demo);
        System.out.println("Serialized JSON:");
        System.out.println(json);
    }

    // 用于执行的 POJO 类
    @Data
    public static class DesensitizeDemo {
        @ChineseNameDesensitize(prefixKeep = 1, suffixKeep = 2, replacer = "*")
        private String nickname;
    }
}

执行结果如下:

在这里插入图片描述

4. 模版

以下只是展示的模版,执行操作请看第二章

@ExtendWith(MockitoExtension.class)
public class DesensitizeTest {

    @Test
    public void test() {
        // 准备参数
        DesensitizeDemo desensitizeDemo = new DesensitizeDemo();
        desensitizeDemo.setNickname("张三");
        desensitizeDemo.setBankCard("6228480402564890018");
        desensitizeDemo.setCarLicense("京A88888");
        desensitizeDemo.setFixedPhone("010-12345678");
        desensitizeDemo.setIdCard("110101199003077172");
        desensitizeDemo.setPassword("password123");
        desensitizeDemo.setPhoneNumber("13812345678");
        desensitizeDemo.setSlider1("ABCDEFG");
        desensitizeDemo.setSlider2("ABCDEFG");
        desensitizeDemo.setSlider3("ABCDEFG");
        desensitizeDemo.setEmail("[email protected]");
        desensitizeDemo.setRegex("这是一条测试数据");
        desensitizeDemo.setAddress("北京市朝阳区XX路XX号");
        desensitizeDemo.setOrigin("初始数据");

        // 调用
        DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class);
        // 断言
        assertNotNull(d);
        assertEquals("张*", d.getNickname());
        assertEquals("622848********0018", d.getBankCard());
        assertEquals("京A8***8", d.getCarLicense());
        assertEquals("010-*****5678", d.getFixedPhone());
        assertEquals("110101********7172", d.getIdCard());
        assertEquals("***********", d.getPassword());
        assertEquals("138****5678", d.getPhoneNumber());
        assertEquals("#######", d.getSlider1());
        assertEquals("ABC*EFG", d.getSlider2());
        assertEquals("*******", d.getSlider3());
        assertEquals("t***@example.com", d.getEmail());
        assertEquals("这是一条****据", d.getRegex());
        assertEquals("北京市朝阳区XX路XX号*", d.getAddress());
        assertEquals("初始数据", d.getOrigin());
    }

    @Data
    public static class DesensitizeDemo {

        @ChineseNameDesensitize
        private String nickname;
        @BankCardDesensitize
        private String bankCard;
        @CarLicenseDesensitize
        private String carLicense;
        @FixedPhoneDesensitize
        private String fixedPhone;
        @IdCardDesensitize
        private String idCard;
        @PasswordDesensitize
        private String password;
        @MobileDesensitize
        private String phoneNumber;
        @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#")
        private String slider1;
        @SliderDesensitize(prefixKeep = 3, suffixKeep = 3)
        private String slider2;
        @SliderDesensitize(prefixKeep = 10)
        private String slider3;
        @EmailDesensitize
        private String email;
        @RegexDesensitize(regex = "这是一条测试数据", replacer = "*")
        private String regex;
        @Address
        private String address;
        private String origin;

    }

}

对应实行各个注解进行脱敏

假设还是刚刚的中文脱敏

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = ChineseNameDesensitization.class)
public @interface ChineseNameDesensitize {

    /**
     * 前缀保留长度
     */
    int prefixKeep() default 1;

    /**
     * 后缀保留长度
     */
    int suffixKeep() default 0;

    /**
     * 替换规则,中文名;比如:吗喽研究僧脱敏之后为码****
     */
    String replacer() default "*";

}

对应的注解如下:

public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler<ChineseNameDesensitize> {

    @Override
    Integer getPrefixKeep(ChineseNameDesensitize annotation) {
        return annotation.prefixKeep();
    }

    @Override
    Integer getSuffixKeep(ChineseNameDesensitize annotation) {
        return annotation.suffixKeep();
    }

    @Override
    String getReplacer(ChineseNameDesensitize annotation) {
        return annotation.replacer();
    }

}

其中改写的函数如下:

public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
        implements DesensitizationHandler<T> {

    @Override
    public String desensitize(String origin, T annotation) {
        int prefixKeep = getPrefixKeep(annotation);
        int suffixKeep = getSuffixKeep(annotation);
        String replacer = getReplacer(annotation);
        int length = origin.length();

        // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换
        if (prefixKeep >= length || suffixKeep >= length) {
            return buildReplacerByLength(replacer, length);
        }

        // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
        if ((prefixKeep + suffixKeep) >= length) {
            return buildReplacerByLength(replacer, length);
        }

        // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
        int interval = length - prefixKeep - suffixKeep;
        return origin.substring(0, prefixKeep) +
                buildReplacerByLength(replacer, interval) +
                origin.substring(prefixKeep + interval);
    }

    /**
     * 根据长度循环构建替换符
     *
     * @param replacer 替换符
     * @param length   长度
     * @return 构建后的替换符
     */
    private String buildReplacerByLength(String replacer, int length) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(replacer);
        }
        return builder.toString();
    }

    /**
     * 前缀保留长度
     *
     * @param annotation 注解信息
     * @return 前缀保留长度
     */
    abstract Integer getPrefixKeep(T annotation);

    /**
     * 后缀保留长度
     *
     * @param annotation 注解信息
     * @return 后缀保留长度
     */
    abstract Integer getSuffixKeep(T annotation);

    /**
     * 替换符
     *
     * @param annotation 注解信息
     * @return 替换符
     */
    abstract String getReplacer(T annotation);

}
;