feat: 支持 vo 返回的脱敏

This commit is contained in:
gaibu 2023-01-12 14:54:33 +08:00
parent e637bff8cd
commit 5c8e41b847
21 changed files with 457 additions and 116 deletions

View File

@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-framework</artifactId>
<version>1.6.6-snapshot</version>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<artifactId>yudao-spring-boot-starter-biz-desensitize</artifactId>
@ -22,5 +22,22 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.framework.desensitize.annotation;
import cn.iocoder.yudao.framework.desensitize.enums.DesensitizationStrategyEnum;
import cn.iocoder.yudao.framework.desensitize.handler.DesensitizationHandler;
import cn.iocoder.yudao.framework.desensitize.serializer.StringDesensitizeSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@ -11,40 +13,18 @@ import java.lang.annotation.Target;
/**
* Desensitize 注解配置会覆盖 DesensitizationStrategyEnum 配置
* Desensitize 顶级脱敏注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = StringDesensitizeSerializer.class)
public @interface Desensitize {
/**
* 脱敏策略
*/
DesensitizationStrategyEnum strategy();
/**
* 脱敏替换字符
*/
String replacer();
/**
* 正则表达式
*/
String regex();
/**
* 前缀保留长度
*/
int preKeep();
/**
* 后缀保留长度
*/
int suffixKeep();
/**
* 脱敏处理器
*/
Class<? extends DesensitizationHandler> handler();
Class<? extends DesensitizationHandler> desensitizationHandler();
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.framework.desensitize.annotation;
import cn.iocoder.yudao.framework.desensitize.handler.RegexDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 正则脱敏注解
*/
@Documented
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitizationHandler = RegexDesensitizationHandler.class)
public @interface RegexDesensitize {
/**
* 匹配的正则表达式默认匹配所有
*/
String regex() default "^[\\s\\S]*$";
/**
* 替换规则会将匹配到的字符串全部替换成 replacer
* 例如regex=123; replacer=******
* 原始字符串 123456789
* 脱敏后字符串 ******456789
*/
String replacer() default "******";
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.desensitize.annotation;
import cn.iocoder.yudao.framework.desensitize.handler.SliderDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 滑动脱敏注解
*/
@Documented
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitizationHandler = SliderDesensitizationHandler.class)
public @interface SliderDesensitize {
/**
* 后缀保留长度
*/
int suffixKeep() default 0;
/**
* 替换规则会将前缀后缀保留后全部替换成 replacer
* 例如prefixKeep = 1; suffixKeep = 2; replacer = "*";
* 原始字符串 123456
* 脱敏后 1***56
*/
String replacer() default "*";
/**
* 前缀保留长度
*/
int prefixKeep() default 0;
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 银行卡号
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 6, suffixKeep = 2, replacer = "*") // 银行卡号;比如9988002866797031脱敏之后为998800********31
public @interface BankCardDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 车牌号
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 3, suffixKeep = 1, replacer = "*") // 车牌号;比如粤A66666脱敏之后为粤A6***6
public @interface CarLicenseDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 中文名
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 1, suffixKeep = 0, replacer = "*") // 中文名;比如刘子豪脱敏之后为刘**
public @interface ChineseNameDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.RegexDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 邮箱
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@RegexDesensitize(regex = "(^.)[^@]*(@.*$)", replacer ="$1****$2") // 邮箱;比如example@gmail.com脱敏之后为e****@gmail.com
public @interface EmailDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 固定电话
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 4, suffixKeep = 2, replacer = "*") // 固定电话;比如01086551122脱敏之后为0108*****22
public @interface FixedPhoneDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 身份证
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 6, suffixKeep = 2, replacer = "*") // 身份证号码;比如530321199204074611脱敏之后为530321**********11
public @interface IdCardDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 密码
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 0, suffixKeep = 0, replacer = "*") // 密码;比如123456脱敏之后为******
public @interface PasswordDesensitize {
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.desensitize.annotation.constraints;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 手机号
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@SliderDesensitize(prefixKeep = 3, suffixKeep = 4, replacer = "*") // 手机号;比如13248765917脱敏之后为132****5917
public @interface PhoneNumberDesensitize {
}

View File

@ -1,24 +0,0 @@
package cn.iocoder.yudao.framework.desensitize.constants;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DesensitizeConstants {
/**
* 默认正则
*/
public static final String DEFAULT_REGEX = null;
/**
* 默认保持长度
*/
public static final int DEFAULT_KEEP_LENGTH = -1;
/**
* 默认替换字符
*/
public static final String DEFAULT_REPLACER = "****";
}

View File

@ -1,49 +0,0 @@
package cn.iocoder.yudao.framework.desensitize.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import static cn.iocoder.yudao.framework.desensitize.constants.DesensitizeConstants.DEFAULT_KEEP_LENGTH;
import static cn.iocoder.yudao.framework.desensitize.constants.DesensitizeConstants.DEFAULT_REGEX;
import static cn.iocoder.yudao.framework.desensitize.constants.DesensitizeConstants.DEFAULT_REPLACER;
@Getter
@RequiredArgsConstructor
public enum DesensitizationStrategyEnum {
// 常用脱敏业务
PHONE_NUMBER(DEFAULT_REGEX, 3, 4, DEFAULT_REPLACER), // 手机号;比如13248765917脱敏之后为132****5917
FIXED_PHONE(DEFAULT_REGEX, 4, 2, DEFAULT_REPLACER), // 固定电话;比如01086551122脱敏之后为0108*****22
ID_CARD(DEFAULT_REGEX, 6, 2, DEFAULT_REPLACER), // 身份证号码;比如530321199204074611脱敏之后为530321**********11
BANK_CARD(DEFAULT_REGEX, 6, 2, DEFAULT_REPLACER), // 银行卡号;比如9988002866797031脱敏之后为998800********31
CHINESE_NAME(DEFAULT_REGEX, 1, 0, "**"),// 中文名;比如刘子豪脱敏之后为刘**
ADDRESS("[\\s\\S]+区", DEFAULT_KEEP_LENGTH, DEFAULT_KEEP_LENGTH, DEFAULT_REPLACER), // 地址只显示到地区不显示详细地址;比如广州市天河区幸福小区102号脱敏之后为广州市天河区********
EMAIL("(^.)[^@]*(@.*$)", DEFAULT_KEEP_LENGTH, DEFAULT_KEEP_LENGTH, "$1****$2"), // 邮箱;比如example@gmail.com脱敏之后为e******@gmail.com
CAR_LICENSE(DEFAULT_REGEX, 3, 1, DEFAULT_REPLACER), // 车牌号;比如粤A66666脱敏之后为粤A6***6
PASSWORD(DEFAULT_REGEX, 0, 0, DEFAULT_REPLACER), // 密码;比如123456脱敏之后为******
// 自定义脱敏业务
REGEX(DEFAULT_REGEX, DEFAULT_KEEP_LENGTH, DEFAULT_KEEP_LENGTH, DEFAULT_REPLACER), // 自定义正则表达式
SLIDE(DEFAULT_REGEX, DEFAULT_KEEP_LENGTH, DEFAULT_KEEP_LENGTH, DEFAULT_REPLACER), // 滑动脱敏
CUSTOM_HANDLE(DEFAULT_REGEX, DEFAULT_KEEP_LENGTH, DEFAULT_KEEP_LENGTH, DEFAULT_REPLACER); // 自定义处理器
;
/**
* 正则表达式
*/
private final String regex;
/**
* 前缀保留长度
*/
private final int preKeep;
/**
* 后缀保留长度
*/
private final int suffixKeep;
/**
* 脱敏替换字符
*/
private final String replacer;
}

View File

@ -1,10 +0,0 @@
package cn.iocoder.yudao.framework.desensitize.handler;
public class DefaultDesensitizationHandler implements DesensitizationHandler {
@Override
public String handle(String origin) {
return origin;
}
}

View File

@ -8,6 +8,6 @@ public interface DesensitizationHandler {
* @param origin 原始字符串
* @return 脱敏后的字符串
*/
String handle(String origin);
String desensitize(String origin, Object... arg);
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.framework.desensitize.handler;
import cn.hutool.core.util.ReflectUtil;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DesensitizationHandlerHolder {
/**
* handler 缓存默认初始化内置的处理器
*/
private static final Map<Class<? extends DesensitizationHandler>, DesensitizationHandler> HANDLER_MAP = new ConcurrentHashMap<>() {{
put(RegexDesensitizationHandler.class, new RegexDesensitizationHandler());
put(SliderDesensitizationHandler.class, new SliderDesensitizationHandler());
}};
public static DesensitizationHandler getDesensitizationHandler(Class<? extends DesensitizationHandler> clazz) {
DesensitizationHandler handler = HANDLER_MAP.get(clazz);
if (handler != null) {
return handler;
}
synchronized (DesensitizationHandlerHolder.class) {
handler = HANDLER_MAP.get(clazz);
// 双重校验锁
if (handler != null) {
return handler;
}
handler = ReflectUtil.newInstanceIfPossible(clazz);
HANDLER_MAP.put(clazz, handler);
}
return handler;
}
}

View File

@ -0,0 +1,13 @@
package cn.iocoder.yudao.framework.desensitize.handler;
public class RegexDesensitizationHandler implements DesensitizationHandler {
@Override
public String desensitize(String origin, Object... arg) {
String regex = (String) arg[0];
String replacer = (String) arg[1];
return origin.replaceAll(regex, replacer);
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.desensitize.handler;
public class SliderDesensitizationHandler implements DesensitizationHandler {
@Override
public String desensitize(String origin, Object... arg) {
int prefixKeep = (Integer) arg[0];
int suffixKeep = (Integer) arg[1];
String replacer = (String) arg[2];
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) {
return String.valueOf(replacer).repeat(Math.max(0, length));
}
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.framework.desensitize.serializer;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.desensitize.annotation.Desensitize;
import cn.iocoder.yudao.framework.desensitize.annotation.RegexDesensitize;
import cn.iocoder.yudao.framework.desensitize.annotation.SliderDesensitize;
import cn.iocoder.yudao.framework.desensitize.handler.DesensitizationHandler;
import cn.iocoder.yudao.framework.desensitize.handler.DesensitizationHandlerHolder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.lang.reflect.Field;
/**
* 脱敏序列化器
*/
public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
private final DesensitizationHandler desensitizationHandler;
protected StringDesensitizeSerializer(DesensitizationHandler desensitizationHandler) {
super(String.class);
this.desensitizationHandler = desensitizationHandler;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
Desensitize annotation = beanProperty.getAnnotation(Desensitize.class);
if (annotation == null) {
return this;
}
return new StringDesensitizeSerializer(DesensitizationHandlerHolder.getDesensitizationHandler(annotation.desensitizationHandler()));
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
if (StrUtil.isBlank(value)) {
gen.writeNull();
return;
}
String currentName = gen.getOutputContext().getCurrentName();
Object currentValue = gen.getCurrentValue();
Class<?> currentValueClass = currentValue.getClass();
Field field = ReflectUtil.getField(currentValueClass, currentName);
// 滑动处理器
SliderDesensitize sliderDesensitize = field.getAnnotation(SliderDesensitize.class);
if (sliderDesensitize != null) {
value = this.desensitizationHandler.desensitize(value, sliderDesensitize.prefixKeep(), sliderDesensitize.suffixKeep(), sliderDesensitize.replacer());
}
// 正则处理器
RegexDesensitize regexDesensitize = field.getAnnotation(RegexDesensitize.class);
if (regexDesensitize != null) {
value = this.desensitizationHandler.desensitize(value, regexDesensitize.regex(), regexDesensitize.replacer());
}
// 自定义处理器
Desensitize desensitize = field.getAnnotation(Desensitize.class);
if (desensitize != null) {
value = this.desensitizationHandler.desensitize(value);
}
gen.writeString(value);
}
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.framework.desensitize.handler;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class DesensitizationHandlerTest {
@Test
public void testSliderDesensitizationHandler() {
DesensitizationHandler handler = DesensitizationHandlerHolder.getDesensitizationHandler(SliderDesensitizationHandler.class);
Assertions.assertEquals("A****FG", handler.desensitize("ABCDEFG", 1, 2, "*"));
Assertions.assertEquals("芋**码", handler.desensitize("芋道源码", 1, 1, "*"));
Assertions.assertEquals("****", handler.desensitize("芋道源码", 4, 0, "*"));
}
@Test
public void testRegexDesensitizationHandler() {
DesensitizationHandler handler = DesensitizationHandlerHolder.getDesensitizationHandler(RegexDesensitizationHandler.class);
Assertions.assertEquals("e****@gmail.com", handler.desensitize("example@gmail.com", "(^.)[^@]*(@.*$)", "$1****$2"));
Assertions.assertEquals("***,铁***", handler.desensitize("他妈的,铁废物", "他妈的|去你大爷|卧槽|草泥马|废物", "***"));
}
}