1、引言
在保护用户隐私的问题上,数据脱敏是一种有效的手段。然而,有时我们需要在运行时动态决定是否进行脱敏,这就需要我们提供一种灵活的、可配置的数据脱敏处理机制。
本文将介绍如何在 Spring 框架中实现这种机制。我们将定义一个新的注解 EnableDesensitize,并在需要脱敏的 Controller 方法上使用它。然后,我们会通过拦截器检查这个注解,并将脱敏的标志存储在 RequestContextHolder 中。最后,在序列化器中检查这个标志,决定是否进行脱敏。
此外,我们还会使用 hutool 工具库来进行数据脱敏,这是一个包含了许多实用功能的 Java 工具类库。
通过这篇文章,你将能理解和实现动态控制数据脱敏的方法,更好地保护用户隐私。
2、 使用hutool工具进行数据脱敏
Hutool 是一个小而全的 Java 工具类库,它为 Java 开发者提供了一系列的工具,包括文件、日期、加密、数据脱敏等功能。在这里,我们将使用 Hutool 的数据脱敏功能来实现我们的数据脱敏处理。
Hutool 提供了DesensitizedUtil
类,其中包含了一系列的静态方法,可以用来对各种类型的数据进行脱敏。例如,我们可以使用 DesensitizedUtil.email
方法来对电子邮件地址进行脱敏,或者使用 DesensitizedUtil.bankCard
方法来对银行卡号进行脱敏。
以下是一个自定义的脱敏类型枚举:
public enum DesensitizedType {
/**
* 用户id
*/
USER_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 座机号
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 密码
*/
PASSWORD,
/**
* 中国大陆车牌,包含普通车辆、新能源车辆
*/
CAR_LICENSE,
/**
* 银行卡
*/
BANK_CARD,
/**
* IPv4地址
*/
IPV4,
/**
* IPv6地址
*/
IPV6,
/**
* 定义了一个first_mask的规则,只显示第一个字符。
*/
FIRST_MASK,
/**
* 清空为null
*/
CLEAR_TO_NULL,
/**
* 清空为""
*/
CLEAR_TO_EMPTY,
/**
* 默认不操作
*/
DEFAULT,
/**
* 自定义规则
*/
CUSTOMIZE_RULE
}
这些脱敏类型用在脱敏注解上:
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveInfoSerializer.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Desensitization {
/**
* 脱敏类型
*/
DesensitizedType type();
/**
* 前置不需要打码的长度
*/
int prefixLen() default 0;
/**
* 后置不需要打码的长度
*/
int suffixLen() default 0;
}
使用了以下注解修饰:
@JacksonAnnotationsInside
:表示该注解是一个 Jackson 注解,并可以作用在其他 Jackson 注解上。
@JsonSerialize(using = SensitiveInfoSerializer.class)
:表示使用 SensitiveInfoSerializer
类来对被标记的字段进行序列化处理,将敏感信息进行脱敏处理。
@Retention(RetentionPolicy.RUNTIME)
:表示该注解在运行时仍然可用。
@Target(ElementType.FIELD)
:表示该注解可以作用在字段上。
在实体类中使用,例如User中:
@Data
public class Student {
@Desensitization(type = DesensitizedType.USER_ID)
private String studentId;
@Desensitization(type = DesensitizedType.CHINESE_NAME)
private String name;
private int age;
@Desensitization(type = DesensitizedType.CUSTOMIZE_RULE,prefixLen = 3,suffixLen = 6)
private String department;
@Desensitization(type = DesensitizedType.MOBILE_PHONE)
private String phoneNumber;
@Desensitization(type = DesensitizedType.EMAIL)
private String email;
@Desensitization(type = DesensitizedType.ADDRESS)
private String address;
}
3、定义脱敏注解 EnableDesensitize
在 Spring 中,我们可以通过自定义注解在Controller层标记需要脱敏的方法。这里,我们将定义一个名为 EnableDesensitize
的注解,用于标记需要进行数据脱敏的 Controller 方法。
下面是 EnableDesensitize
注解的定义:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableDesensitize {
}
这个注解的定义中,@Target 指定了这个注解可以应用到的 Java 元素类型,这里我们允许它应用到方法(ElementType.METHOD
)。@Retention
则指定了这个注解的生命周期,RetentionPolicy.RUNTIME
意味着这个注解在运行时仍然有效,这样我们就可以在运行时通过反射来检查这个注解。
有了这个注解,我们就可以在 Controller 方法上使用它来标记需要进行数据脱敏的方法,如下所示:
@EnableDesensitize
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
// ...
}
在这个例子中,getUser()
方法被标记为需要进行数据脱敏,因此当这个方法被调用时,我们需要对其返回的 User 对象进行脱敏处理。
4、实现脱敏拦截器
在 Spring 中,我们可以通过实现 HandlerInterceptor
接口来创建一个拦截器,这个拦截器可以在处理 HTTP 请求之前、之后或者在视图渲染之前进行拦截操作。在这个例子中,我们将创建一个拦截器,用于在处理请求之前检查 EnableDesensitize
注解,并将这个信息存储在 RequestContextHolder
中。
首先,我们需要创建一个新的类,这个类需要实现 HandlerInterceptor
接口,并覆盖 preHandle
方法:
public class DesensitizeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
EnableDesensitize annotation = handlerMethod.getMethodAnnotation(EnableDesensitize.class);
if (annotation != null) {
// 如果方法上有 EnableDesensitize 注解,那么将一个标记存入 RequestContextHolder
RequestContextHolder.getRequestAttributes().setAttribute("enableDesensitize", true, RequestAttributes.SCOPE_REQUEST);
}
}
return true;
}
}
在这个 preHandle
方法中,我们首先检查 handler 是否是 HandlerMethod
的实例。如果是,那么我们就可以通过 getMethodAnnotation
方法来获取方法上的 EnableDesensitize
注解。如果这个注解存在,那么我们就将一个名为 “enableDesensitize” 的标记存入 RequestContextHolder
。
然后,我们需要在 Spring 的配置中注册这个拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DesensitizeInterceptor());
}
}
在这个配置中,我们通过 addInterceptor
方法将我们的 DesensitizeInterceptor
添加到了拦截器的注册表中。这样,每次处理 HTTP 请求时,都会先调用这个拦截器的 preHandle
方法。
有了这个拦截器,我们就可以在处理请求之前检查 EnableDesensitize
注解,并将这个信息存储在 RequestContextHolder
中,以便在之后的处理中使用。
5、实现脱敏序列化器
在 Spring 中,我们可以通过实现 JsonSerializer
接口并结合 ContextualSerializer
接口来创建一个自定义的序列化器。在这个例子中,我们将创建一个序列化器,用于在序列化数据时进行脱敏处理。
首先,我们需要创建一个新的类,这个类需要继承 StdSerializer<String>
并实现 ContextualSerializer
接口,覆盖 createContextual
和 serialize
方法:
public class SensitiveInfoSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DesensitizedType type;
private int prefixLen;
private int suffixLen;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
boolean enableDesensitize = false;
if (requestAttributes != null) {
enableDesensitize = Convert.toBool(requestAttributes.getAttribute("enableDesensitize", RequestAttributes.SCOPE_REQUEST), false);
}
if (enableDesensitize && value != null && type != null) {
String newStr = value;
switch (type) {
case USER_ID:
newStr = String.valueOf(DesensitizedUtil.userId());
break;
case CHINESE_NAME:
newStr = DesensitizedUtil.chineseName(value);
break;
case ID_CARD:
newStr = DesensitizedUtil.idCardNum(value, 1, 2);
break;
case FIXED_PHONE:
newStr = DesensitizedUtil.fixedPhone(value);
break;
case MOBILE_PHONE:
newStr = DesensitizedUtil.mobilePhone(value);
break;
case ADDRESS:
newStr = DesensitizedUtil.address(value, 8);
break;
case EMAIL:
newStr = DesensitizedUtil.email(value);
break;
case PASSWORD:
newStr = DesensitizedUtil.password(value);
break;
case CAR_LICENSE:
newStr = DesensitizedUtil.carLicense(value);
break;
case BANK_CARD:
newStr = DesensitizedUtil.bankCard(value);
break;
case IPV4:
newStr = DesensitizedUtil.ipv4(value);
break;
case IPV6:
newStr = DesensitizedUtil.ipv6(value);
break;
case FIRST_MASK:
newStr = DesensitizedUtil.firstMask(value);
break;
case CLEAR_TO_EMPTY:
newStr = DesensitizedUtil.clear();
break;
case CLEAR_TO_NULL:
newStr = DesensitizedUtil.clearToNull();
break;
case DEFAULT:
newStr = value;
break;
case CUSTOMIZE_RULE:
newStr = StrUtil.hide(value, prefixLen, suffixLen);
break;
}
gen.writeString(newStr);
} else {
gen.writeObject(value);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
if (property != null) {
Desensitization desensitization = property.getAnnotation(Desensitization.class);
if (desensitization != null) {
this.type = desensitization.type();
this.prefixLen = desensitization.prefixLen();
this.suffixLen = desensitization.suffixLen();
}
}
return this;
}
}
这段代码定义了一个脱敏序列化器 SensitiveInfoSerializer
,它继承自JsonSerializer<String>
并实现了ContextualSerializer
接口。这个序列化器可以对字符串类型的字段进行脱敏处理。
以下是对代码的详细解释:
enableDesensitize
:决定是否启用脱敏。
type
:脱敏的类型,由 DesensitizedType
枚举定义。
prefixLen
和 suffixLen
:自定义规则脱敏时,保留的前缀和后缀的长度。
serialize
方法:这是 JsonSerializer
接口的核心方法,用于将对象序列化为 JSON。在这个方法中,如果启用了脱敏并且值不为空,且脱敏类型不为空,那么根据脱敏类型进行脱敏处理。否则,直接将原始值写入 JSON。
createContextual
方法:这是 ContextualSerializer
接口的方法,用于在运行时确定如何序列化字段。在这个方法中,检查字段是否有 Desensitization 注解,如果有,那么获取注解中定义的脱敏类型、前缀长度和后缀长度,并设置到序列化器中。
6、效果
测试接口
@GetMapping("/getUser1")
@EnableDesensitize
public R getUser1(){
User userInfo = getUserInfo();
return R.ok("脱敏后的数据").put("data", userInfo);
}
@GetMapping("/getUser2")
public R getUser2(){
User userInfo = getUserInfo();
return R.ok("未脱敏的数据").put("data", userInfo);
}
private User getUserInfo(){
User user = new User();
user.setStudentId("2002210");
user.setName("张三");
user.setAge(18);
user.setEmail("zhangsan@example.com");
user.setPhoneNumber("13800138000");
user.setDepartment("计算机学院");
user.setAddress("北京市海淀区颐和园路5号");
return user;
}
脱敏后的数据:
未脱敏的数据:
7、总结
没有,你们自己总结吧。