一个注解让 Spring Boot 项目接口返回数据脱敏

(给ImportNew加星标,提高Java技能)


1、背景


需求是某些接口返回的信息,涉及到敏感数据的必须进行脱敏操作。


2 思路


  1. 要做成可配置多策略的脱敏操作,要不然一个个接口进行脱敏操作,重复的工作量太多,很显然违背了“多写一行算我输”的程序员规范。思来想去,定义数据脱敏注解和数据脱敏逻辑的接口, 在返回类上,对需要进行脱敏的属性加上,并指定对应的脱敏策略操作。

  2. 接下来我只需要拦截控制器返回的数据,找到带有脱敏注解的属性操作即可,一开始打算用 @ControllerAdvice 去实现,但发现需要自己去反射类获取注解。当返回对象比较复杂,需要递归去反射,性能一下子就会降低,于是换种思路,我想到平时使用的 @JsonFormat,跟我现在的场景很类似,通过自定义注解跟字段解析器,对字段进行自定义解析,tql。


3 实现代码


3.1 自定义数据注解,并可以配置数据脱敏策略


package com.wkf.workrecord.tools.desensitization; import java.lang.annotation.*; /** * 注解类 * @author wuKeFan * @date 2023-02-20 09:36:39 */ @Target({ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface DataMasking {     DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK; }


3.2 自定义 Serializer


参考 jackson 的 StringSerializer,下面的示例只针对 String 类型进行脱敏。


DataMaskingOperation.class:


package com.wkf.workrecord.tools.desensitization; /** * 接口脱敏操作接口类 * @author wuKeFan * @date 2023-02-20 09:37:48 */public interface DataMaskingOperation {     String MASK_CHAR = "*";     String mask(String content, String maskChar); }


DataMaskingFunc.class:


package com.wkf.workrecord.tools.desensitization; import org.springframework.util.StringUtils; /** * 脱敏转换操作枚举类 * @author wuKeFan * @date 2023-02-20 09:38:35 */public enum DataMaskingFunc {     /**     *  脱敏转换器     */    NO_MASK((str, maskChar) -> {        return str;    }),    ALL_MASK((str, maskChar) -> {        if (StringUtils.hasLength(str)) {            StringBuilder sb = new StringBuilder();            for (int i = 0; i < str.length(); i++) {                sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);            }            return sb.toString();        } else {            return str;        }    });     private final DataMaskingOperation operation;     private DataMaskingFunc(DataMaskingOperation operation) {        this.operation = operation;    }     public DataMaskingOperation operation() {        return this.operation;    } }

DataMaskingSerializer.class:


package com.wkf.workrecord.tools.desensitization; import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.databind.JavaType;import com.fasterxml.jackson.databind.JsonMappingException;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.SerializerProvider;import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;import com.fasterxml.jackson.databind.jsontype.TypeSerializer;import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; import java.io.IOException;import java.util.Objects; /** * 自定义Serializer * @author wuKeFan * @date 2023-02-20 09:39:47 */public final class DataMaskingSerializer extends StdScalarSerializer<Object> {    private final DataMaskingOperation operation;     public DataMaskingSerializer() {        super(String.class, false);        this.operation = null;    }     public DataMaskingSerializer(DataMaskingOperation operation) {        super(String.class, false);        this.operation = operation;    }      public boolean isEmpty(SerializerProvider prov, Object value) {        String str = (String)value;        return str.isEmpty();    }     public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {        if (Objects.isNull(operation)) {            String content = DataMaskingFunc.ALL_MASK.operation().mask((String) value, null);            gen.writeString(content);        } else {            String content = operation.mask((String) value, null);            gen.writeString(content);        }    }     public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {        this.serialize(value, gen, provider);    }     public JsonNode getSchema(SerializerProvider provider) {        return this.createSchemaNode("string", true);    }     public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {        this.visitStringFormat(visitor, typeHint);    }}

3.3 自定义 AnnotationIntrospector,适配我们自定义注解返回相应的 Serializer

 

package com.wkf.workrecord.tools.desensitization; import com.fasterxml.jackson.databind.introspect.Annotated;import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;import lombok.extern.slf4j.Slf4j; /** * @author wuKeFan * @date 2023-02-20 09:43:41 */@Slf4jpublic class DataMaskingAnnotationIntroSpector extends NopAnnotationIntrospector {     @Override    public Object findSerializer(Annotated am) {        DataMasking annotation = am.getAnnotation(DataMasking.class);        if (annotation != null) {            return new DataMaskingSerializer(annotation.maskFunc().operation());        }        return null;    } }

3.4 覆盖 ObjectMapper


package com.wkf.workrecord.tools.desensitization; import com.fasterxml.jackson.databind.AnnotationIntrospector;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** * 覆盖 ObjectMapper * @author wuKeFan * @date 2023-02-20 09:44:35 */@Configuration(proxyBeanMethods = false)public class DataMaskConfiguration {     @Configuration(proxyBeanMethods = false)    @ConditionalOnClass({Jackson2ObjectMapperBuilder.class})    static class JacksonObjectMapperConfiguration {        JacksonObjectMapperConfiguration() {        }         @Bean        @Primary        ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {            ObjectMapper objectMapper = builder.createXmlMapper(false).build();            AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();            AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntroSpector());            objectMapper.setAnnotationIntrospector(newAi);            return objectMapper;        }    } }

3.5 返回对象加上注解


package com.wkf.workrecord.tools.desensitization; import lombok.Data; import java.io.Serializable; /** * 需要脱敏的实体类 * @author wuKeFan * @date 2023-02-20 09:35:52 */@Datapublic class User implements Serializable {    /**     * 主键ID     */    private Long id;     /**     * 姓名     */    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)    private String name;     /**     * 年龄     */    private Integer age;     /**     * 邮箱     */    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)    private String email; }


4 测试


我们写一个Controller测试一下看是不是我们需要的效果。


4.1 测试的 Controller 类 DesensitizationController.class


代码如下:


package com.wkf.workrecord.tools.desensitization; import com.biboheart.brick.model.BhResponseResult;import com.wkf.workrecord.utils.ResultVOUtils;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController; /** * 测试接口脱敏测试控制类 * @author wuKeFan * @date 2022-06-21 17:23 */@Slf4j@RestController@RequiredArgsConstructor@RequestMapping("/desensitization/")public class DesensitizationController {     @RequestMapping(value = "test", method = {RequestMethod.GET, RequestMethod.POST})    public BhResponseResult<User> test() {        User user = new User();        user.setAge(1);        user.setEmail("123456789@qq.com");        user.setName("吴名氏");        user.setId(1L);        return ResultVOUtils.success(user);    } }


4.2 PostMan 接口请求


效果符合预期,如下图所示:


转自:吴名氏.,

链接:blog.csdn.net/qq_37284798/article/details/129118284


- EOF -

推荐阅读  点击标题可跳转

1、太强了!一个注解解决数据脱敏问题

2、MyBatis 插件 + 注解 轻松实现数据脱敏

3、logback - 自定义日志脱敏组件,一种不错的脱敏方案


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️


相关推荐

  • VS Code 官网跳转到反诈提醒页面
  • 代码是上午写的,人是下午被开除的
  • Spring Bean 名称暗藏玄机,这样取名就不会被代理
  • 《图解线性代数》中文版.PDF
  • 裁员 14000 人!
  • Java程序员面试应该准备什么?
  • DDD实战:应对并发挑战,五个技巧让你轻松应对
  • 科大讯飞!起飞了!
  • starrock通过导入实现数据变更
  • 1024 程序员节全体大会正式开幕:院士、技术英雄齐聚,看开源如何启动 AI 新纪元!
  • 华为辟谣将发射 1 万枚 6G 移动低轨卫星;雷军称新十年成为全球硬核科技引领者;首个软件专利获得者马丁·格茨离世|极客头条
  • 涨停在吃35%落袋,这3之只加入自选
  • 脱发秘籍:前端Chrome调试技巧最全汇总
  • uni-app x 来了 !!!
  • 找对方法,Rust 也可以很简单
  • JavaScript 实在太烂了!!!
  • AI时代下的管理变革
  • 你知道 Python 其实自带了小型数据库吗
  • MyBatis-Plus实现逻辑删除[MyBatis-Plus系列] - 492篇
  • Apollo,真香!