数据脱敏也叫数据的去隐私化,在我们给定脱敏规则和策略的情况下,对敏感数据比如 手机号、银行卡号 等信息,进行转换或者修改的一种技术手段,防止敏感数据直接在不可靠的环境下使用。
由于spring 接口返回JSON数据是使用的Jackson组件;我们可以利用Jackson+自定义注解的方式去实现数据脱敏;实现接口返回数据自动进行数据脱敏
一、导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-system</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.9</version>
</dependency>
二、代码实现
1、新建一个顶层的脱敏接口
import java.util.function.Function;
/**
* 自定义数据脱敏可实现当前接口 或直接在 SensitiveStrategy 中添加枚举
*
* @author minjianguo
* @date 2023/08/09
*/
@FunctionalInterface
public interface IDesensitizeRule {
/**
* 脱敏操作
*
* @return {@link String}
*/
Function<String, String> desensitize();
}
2、创建一个枚举类并继承接口
利用枚举类+函数式接口代替普通策略模式更加方便快捷
import cn.hutool.core.util.DesensitizedUtil;
import com.poctip.encryption.util.UtilTools;
import lombok.AllArgsConstructor;
import java.util.function.Function;
/**
* 敏感战略
* 脱敏策略
*
* @author minjianguo
* @version 3.6.0
* @date 2023/08/09
*/
@AllArgsConstructor
public enum SensitiveStrategy implements IDesensitizeRule {
DEFAULT(s -> s),
/**
* 身份证脱敏
*/
ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)),
/**
* 手机号脱敏
*/
PHONE(DesensitizedUtil::mobilePhone),
/**
* 地址脱敏
*/
ADDRESS(s -> UtilTools.encryption(s, 2, 0)),
/**
* 邮箱脱敏
*/
EMAIL(DesensitizedUtil::email),
/**
* 银行卡
*/
BANK_CARD(DesensitizedUtil::bankCard),
/**
* 名字
* 适配原有加密组件
*/
NAME(s -> UtilTools.encryption(s, 1, 0)),
TELEPHONE(s -> UtilTools.encryption(s, 1, 3));
//可自行添加其他脱敏策略
private final Function<String, String> desensitizer;
/**
* 脱敏操作
*
* @return {@link String}
*/
@Override
public Function<String, String> desensitize() {
return desensitizer;
}
}
3、自定义注解
strategy
: 数据脱敏策略isCustomRule
:是否启用自定义的规则customRule
:自定义的实现类
默认使用 strategy
枚举里面的策略,如果要新增新的数据脱敏规则可直接在 SensitiveStrategy
类新增脱敏规则枚举;(如果要把这个数据脱敏组件打成一个jar包的时候就没办法在 **SensitiveStrategy**
里面新增枚举了;只能采用另一种方式新增自定义脱敏规则)示例如下:
- 新增脱敏规则类实现 IDesensitizeRule 接口,重写
desensitize
方法。 - 在需要脱敏的字段标注
@Sensitive(isCustomRule = true,customRule = UnknownDesensitizeRule.class)
UnknownDesensitizeRule
为自己的脱敏规则类 即可。\
@JacksonAnnotationsInside是一个元注解,它用于注解其他注解,以指示这些注解可以用于Jackson库的注解处理器中。
@JsonSerialize(using = SensitiveHandler.class)是一个用于属性或字段的注解,它指示Jackson库在序列化过程中使用自定义的SensitiveHandler类进行处理。
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.poctip.common.sensitive.core.IDesensitizeRule;
import com.poctip.common.sensitive.core.SensitiveStrategy;
import com.poctip.common.sensitive.core.UnknownDesensitizeRule;
import com.poctip.common.sensitive.handler.SensitiveHandler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 敏感
* 数据脱敏注解
*
* @author
* @date 2023/08/09
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveHandler.class)
public @interface Sensitive {
/**
* 策略
*
* @return {@link SensitiveStrategy}
*/
SensitiveStrategy strategy() default SensitiveStrategy.DEFAULT;
/**
* 是否自定义规则
*
* @return boolean
*/
boolean isCustomRule() default false;
/**
* 自定义规则实现类
*
* @return {@link Class}<{@link ?} {@link extends} {@link IDesensitizeRule}>
*/
Class<? extends IDesensitizeRule> customRule() default UnknownDesensitizeRule.class;
}
SensitiveMethodMask
:标注在要数据脱敏的接口方法上exclude
:排除当前接口不需要脱敏的字段
/**
* 敏感方法面具
*
* @author minjianguo
* @date 2023/08/03
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMethodMask {
String[] exclude() default {};
}
3.1、创建一个默认的脱敏实现类;不做任何脱敏操作 UnknownDesensitizeRule;目的是为了给注解一个默认值
import java.util.function.Function;
/**
* 不处理数据脱敏
*
* @author minjianguo
* @date 2023/08/09
*/
public class UnknownDesensitizeRule implements IDesensitizeRule{
/**
* 脱敏操作
*
* @return {@link String}
*/
@Override
public Function<String, String> desensitize() {
return s -> s;
}
}
4、核心逻辑代码
创建一个数据脱敏上下文SensitiveContextHolder
通过 SensitiveContextHolder
和 spring 的 拦截器配合使用实现在当前接口总启用或关闭或排除某些字段不进行序列化
/**
* 上下文敏感持有人
*
* @author minjianguo
* @date 2023/08/03
*/
public class SensitiveContextHolder {
private static final ThreadLocal<Boolean> THREAD_LOCAL = ThreadLocal.withInitial(() -> false);
private static final ThreadLocal<String[]> FIELD_THREAD_LOCAL = ThreadLocal.withInitial(() -> new String[0]);
/**
* 指定字段不脱敏
*
* @param fields 字段
*/
public static void filter(String... fields) {
if (Objects.isNull(fields)) return;
FIELD_THREAD_LOCAL.set(fields);
}
/**
* 得到过滤字段
*
* @return {@link String[]}
*/
public static String[] getFilterFields() {
return FIELD_THREAD_LOCAL.get();
}
public static boolean isDisable() {
return THREAD_LOCAL.get();
}
public static void enable() {
THREAD_LOCAL.set(true);
}
public static boolean isSensitive() {
return THREAD_LOCAL.get();
}
/**
* 禁用
*/
public static void disable() {
THREAD_LOCAL.set(true);
}
/**
* 清晰
*/
public static void clear() {
THREAD_LOCAL.remove();
FIELD_THREAD_LOCAL.remove();
}
}
新增一个Jackson序列化器
在Jackson库中,JsonSerializer是一个抽象类,用于自定义对象的序列化过程。它将Java对象转换为JSON字符串的过程中,允许开发人员对序列化过程进行自定义控制,并可以处理一些特定的序列化需求。
ContextualSerializer是Jackson库中的一个接口,用于定义上下文相关的序列化器。它扩展了JsonSerializer接口,并添加了一个额外的方法createContextual()。
ContextualSerializer接口允许序列化器在序列化过程中获取上下文信息,并根据上下文进行动态配置或自定义序列化逻辑。它提供了一种在序列化过程中动态决定序列化器行为的机制。
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonStreamContext;
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.poctip.common.sensitive.annotation.Sensitive;
import com.poctip.common.sensitive.context.SensitiveContextHolder;
import com.poctip.common.sensitive.core.IDesensitizeRule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
/**
* 数据脱敏json序列化工具
*
* @author Yjoioooo
*/
@Slf4j
public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer {
private IDesensitizeRule strategy;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
String[] filterFields = SensitiveContextHolder.getFilterFields();
JsonStreamContext outputContext = gen.getOutputContext();
String currentName = outputContext.getCurrentName();
boolean noneMatch = Arrays.stream(filterFields).noneMatch(s -> StrUtil.equals(s, currentName));
boolean isSensitive = SensitiveContextHolder.isSensitive() && noneMatch;
if (isSensitive) {
gen.writeString(strategy.desensitize().apply(value));
} else {
gen.writeString(value);
}
} catch (Exception e) {
log.error("脱敏失败 => {}", e.getMessage());
gen.writeString(value);
}
}
@SneakyThrows
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
if (annotation.isCustomRule()) {
Class<? extends IDesensitizeRule> rule = annotation.customRule();
this.strategy = rule.getDeclaredConstructor().newInstance();
return this;
}
this.strategy = annotation.strategy();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
实现 spring 拦截器
步骤:
- 在进入请求前判断当前接口是否要开启数据脱敏功能。
- 在数据序列化完成之后 清除上下文,防止内存泄露。
import com.poctip.common.sensitive.annotation.SensitiveMethodMask;
import com.poctip.common.sensitive.context.SensitiveContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class SensitiveInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在 MappingJackson2HttpMessageConverter 后执行的自定义方法
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
SensitiveMethodMask annotation = getSensitiveMethodMaskAnnotation(handlerMethod);
if (annotation != null) {
enableSensitiveContext(annotation);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (handler instanceof HandlerMethod) {
if (getSensitiveMethodMaskAnnotation((HandlerMethod) handler) != null) {
SensitiveContextHolder.clear();
}
}
}
/**
* 启用敏感上下文。
*
* @param annotation SensitiveMethodMask注解
*/
private void enableSensitiveContext(SensitiveMethodMask annotation) {
SensitiveContextHolder.enable();
SensitiveContextHolder.filter(annotation.exclude());
}
/**
* 从处理方法中获取SensitiveMethodMask注解。
*
* @param handlerMethod 处理方法
* @return SensitiveMethodMask注解,如果不存在则返回null
*/
private SensitiveMethodMask getSensitiveMethodMaskAnnotation(HandlerMethod handlerMethod) {
return handlerMethod.getMethod().getAnnotation(SensitiveMethodMask.class);
}
}
新增一个手动脱敏的工具类
在某些场景下可以手动去处理数据脱敏。
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ReflectUtil;
import com.poctip.common.sensitive.annotation.Sensitive;
import com.poctip.common.sensitive.core.SensitiveStrategy;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
/**
* 脱敏工具类
*
* @author minjianguo
* @date 2022/12/02
*/
public class SensitiveUtils {
/**
* 数据脱敏
*/
@SneakyThrows
public static <T> void dataDesensitization(T t) {
if (Objects.isNull(t)) return;
Field[] fields = ReflectUtil.getFields(t.getClass(), field -> field.isAnnotationPresent(Sensitive.class));
for (Field field : fields) {
Object fieldValue = ReflectUtil.getFieldValue(t, field);
if (Objects.isNull(fieldValue)) {
continue;
}
Sensitive sensitive = AnnotationUtil.getAnnotation(field, Sensitive.class);
SensitiveStrategy strategy = sensitive.strategy();
String desensitizationString = strategy.desensitize().apply(Convert.convert(String.class,fieldValue));
if (StringUtils.isNotBlank(desensitizationString)) {
ReflectUtil.setFieldValue(t, field, desensitizationString);
}
}
}
/**
* 数据脱敏列表
*
* @param list 列表
*/
public static <T> void dataDesensitizationList(List<T> list){
list.forEach(SensitiveUtils::dataDesensitization);
}
@SneakyThrows
public static String dataDesensitizationString(String value, SensitiveStrategy strategy) {
if (StringUtils.isBlank(value) || Objects.isNull(strategy)) {
return value;
}
return strategy.desensitize().apply(value);
}
}
三、配置spring 拦截器
配置自定义的拦截器。
@Configuration
public class SensitiveConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SensitiveInterceptor())
.addPathPatterns("/**"); // 这里可以指定拦截的路径
}
}
四、使用
在需要数据脱敏的实体类字段标注注解 @Sensitive(
,并指定脱敏策略
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phone;
在接口方法上标注 @SensitiveMethodMask
注解
@SensitiveMethodMask
@GetMapping("/test")
public UserDemo test() {
UserDemo userDemo = new UserDemo();
userDemo.setId(1);
userDemo.setName("java");
userDemo.setPhone("18160360561");
return userDemo;
}
测试返回:
{
"id": 1,
"name": "java",
"phone": "181****0561"
}
注意事项:
接口的返回值不能是 object 或 Map 等类型,不然在序列化时无法找到 **@Sensitive**
注解使数据脱敏功能失效,一定要正确指定返回的数据类型。