目录
- 前言
- 1. 基本知识
- 2. 核心逻辑
- 3. Demo
- 4. 模版
前言
对于隐私信息,需要做特殊处理,比如身份证或者手机号等
对于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("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());
}
@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);
}