在spring boot中利用自定义注解+hutool工具+自定义序列化器来实现注解脱敏(动态控制数据脱敏处理的实现)

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 枚举定义。

prefixLensuffixLen:自定义规则脱敏时,保留的前缀和后缀的长度。

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、总结

没有,你们自己总结吧。

  • 50
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要通过自定义注解和AOP来实现Spring Security配置指定接口不需要Token才能访问,可以按照以下步骤进行操作: 1. 创建一个自定义注解,例如`@NoTokenRequired`,用于标识不需要Token的接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoTokenRequired { } ``` 2. 创建一个切面类,用于拦截带有`@NoTokenRequired`注解的方法,并跳过Spring Security的Token验证。 ```java @Aspect @Component public class TokenValidationAspect { @Before("@annotation(com.example.NoTokenRequired)") public void skipTokenValidation(JoinPoint joinPoint) { // 跳过Spring Security的Token验证逻辑 SecurityContextHolder.getContext().setAuthentication(null); } } ``` 3. 配置Spring Security,将AOP切面类添加到Spring Security的配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenValidationAspect tokenValidationAspect; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 配置需要Token验证的接口 .anyRequest().authenticated() .and() .csrf().disable(); // 将AOP切面类添加到Spring Security的配置 http.addFilterBefore(tokenValidationAspect, UsernamePasswordAuthenticationFilter.class); } } ``` 4. 在需要不需要Token验证的接口上,添加`@NoTokenRequired`注解。 ```java @RestController public class ExampleController { @NoTokenRequired @GetMapping("/example") public String example() { return "This API does not require Token"; } } ``` 这样配置之后,带有`@NoTokenRequired`注解的接口将不会进行Spring Security的Token验证,即可在没有Token的情况下访问该接口。其他接口仍然需要进行Token验证。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值