java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Springboot Jackson数据脱敏

Springboot+Jackson自定义注解数据脱敏的项目实践

作者:少年酱105974

数据脱敏可以对敏感数据比如 手机号、银行卡号等信息进行转换或者修改,本文主要介绍了Springboot+Jackson 自定义注解数据脱敏,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

数据脱敏也叫数据的去隐私化,在我们给定脱敏规则和策略的情况下,对敏感数据比如 手机号、银行卡号 等信息,进行转换或者修改的一种技术手段,防止敏感数据直接在不可靠的环境下使用。

由于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 枚举里面的策略,如果要新增新的数据脱敏规则可直接在 SensitiveStrategy 类新增脱敏规则枚举;(如果要把这个数据脱敏组件打成一个jar包的时候就没办法在 **SensitiveStrategy** 里面新增枚举了;只能采用另一种方式新增自定义脱敏规则)示例如下:

@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;  
}  
/**  
* 敏感方法面具  
*  
* @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序列化器

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** 注解使数据脱敏功能失效,一定要正确指定返回的数据类型。

到此这篇关于Springboot+Jackson自定义注解数据脱敏的项目实践的文章就介绍到这了,更多相关Springboot Jackson数据脱敏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文