目录
- 前言
- 1. 基本知识
- 2. 核心逻辑
- 3. Demo
- 4. 模版
前言
对于隐私信息,需要做特殊处理,比如身份证或者手机号等
对于Java的相关知识推荐阅读:java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
1. 基本知识
脱敏(Desensitization)指在保持数据结构不变的前提下,对敏感数据进行处理,使其不再具备直接识别个人身份或敏感信息的能力,从而保护用户隐私
常见的脱敏方法包括:
- 替换(Masking):将敏感数据中的一部分或全部字符替换为特定字符,如将姓名中的一部分字符替换为星号
- 截断(Truncation):截取敏感数据的一部分,只保留部分信息,如只保留电话号码的前几位
- 加密(Encryption):使用算法将敏感数据转换为密文,只有经过解密才能还原为原始数据
- 哈希(Hashing):将敏感数据通过哈希算法转换为固定长度的哈希值,不可逆转
序列化器是指在将对象转换为字节流或其他格式时,负责对对象进行序列化的组件。在脱敏处理中,序列化器可以通过自定义的逻辑对敏感数据进行处理,使其在序列化过程中不泄露隐私信息
主要将其自定义注解继承自 Jackson 库中的 JsonSerializer 类,在序列化过程中做一定的处理
2. 核心逻辑
在定义的字段中加入自定义注解类
类似如下:
@Data
public static class DesensitizeDemo {@ChineseNameDesensitizeprivate String nickname;
}
对应注解的核心内容如下:
// 脱敏注解
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = ChineseNameDesensitization.Serializer.class)
@interface ChineseNameDesensitize {int prefixKeep() default 1; // 前缀保留的字符数,默认为1int suffixKeep() default 2; // 后缀保留的字符数,默认为2String 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; // 前缀保留的字符数,默认为1int suffixKeep() default 2; // 后缀保留的字符数,默认为2String 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;}// 序列化方法@Overridepublic 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 类@Datapublic static class DesensitizeDemo {@ChineseNameDesensitize(prefixKeep = 1, suffixKeep = 2, replacer = "*")private String nickname;}
}
执行结果如下:
4. 模版
以下只是展示的模版,执行操作请看第二章
@ExtendWith(MockitoExtension.class)
public class DesensitizeTest {@Testpublic 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("test@example.com");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());}@Datapublic static class DesensitizeDemo {@ChineseNameDesensitizeprivate String nickname;@BankCardDesensitizeprivate String bankCard;@CarLicenseDesensitizeprivate String carLicense;@FixedPhoneDesensitizeprivate String fixedPhone;@IdCardDesensitizeprivate String idCard;@PasswordDesensitizeprivate String password;@MobileDesensitizeprivate 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;@EmailDesensitizeprivate String email;@RegexDesensitize(regex = "这是一条测试数据", replacer = "*")private String regex;@Addressprivate 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> {@OverrideInteger getPrefixKeep(ChineseNameDesensitize annotation) {return annotation.prefixKeep();}@OverrideInteger getSuffixKeep(ChineseNameDesensitize annotation) {return annotation.suffixKeep();}@OverrideString getReplacer(ChineseNameDesensitize annotation) {return annotation.replacer();}}
其中改写的函数如下:
public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>implements DesensitizationHandler<T> {@Overridepublic 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);}