如何在SpringBoot项目上让接口返回数据脱敏,一个注解即可

ゞ 浴缸里的玫瑰 2023-10-04 21:38 88阅读 0赞

1 背景

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

2 思路

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

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

3 实现代码

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

  1. package com.wkf.workrecord.tools.desensitization;
  2. import java.lang.annotation.*;
  3. /**
  4. * 注解类
  5. * @author wuKeFan
  6. * @date 2023-02-20 09:36:39
  7. */
  8. @Target({ElementType.FIELD, ElementType.TYPE})
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Documented
  11. public @interface DataMasking {
  12. DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;
  13. }

3.2 自定义 Serializer,参考 jackson 的 StringSerializer,下面的示例只针对 String 类型进行脱敏

DataMaskingOperation.class:

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

DataMaskingFunc.class:

  1. package com.wkf.workrecord.tools.desensitization;
  2. import org.springframework.util.StringUtils;
  3. /**
  4. * 脱敏转换操作枚举类
  5. * @author wuKeFan
  6. * @date 2023-02-20 09:38:35
  7. */
  8. public enum DataMaskingFunc {
  9. /**
  10. * 脱敏转换器
  11. */
  12. NO_MASK((str, maskChar) -> {
  13. return str;
  14. }),
  15. ALL_MASK((str, maskChar) -> {
  16. if (StringUtils.hasLength(str)) {
  17. StringBuilder sb = new StringBuilder();
  18. for (int i = 0; i < str.length(); i++) {
  19. sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
  20. }
  21. return sb.toString();
  22. } else {
  23. return str;
  24. }
  25. });
  26. private final DataMaskingOperation operation;
  27. private DataMaskingFunc(DataMaskingOperation operation) {
  28. this.operation = operation;
  29. }
  30. public DataMaskingOperation operation() {
  31. return this.operation;
  32. }
  33. }

DataMaskingSerializer.class:

  1. package com.wkf.workrecord.tools.desensitization;
  2. import com.fasterxml.jackson.core.JsonGenerator;
  3. import com.fasterxml.jackson.databind.JavaType;
  4. import com.fasterxml.jackson.databind.JsonMappingException;
  5. import com.fasterxml.jackson.databind.JsonNode;
  6. import com.fasterxml.jackson.databind.SerializerProvider;
  7. import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
  8. import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
  9. import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
  10. import java.io.IOException;
  11. import java.util.Objects;
  12. /**
  13. * 自定义Serializer
  14. * @author wuKeFan
  15. * @date 2023-02-20 09:39:47
  16. */
  17. public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
  18. private final DataMaskingOperation operation;
  19. public DataMaskingSerializer() {
  20. super(String.class, false);
  21. this.operation = null;
  22. }
  23. public DataMaskingSerializer(DataMaskingOperation operation) {
  24. super(String.class, false);
  25. this.operation = operation;
  26. }
  27. public boolean isEmpty(SerializerProvider prov, Object value) {
  28. String str = (String)value;
  29. return str.isEmpty();
  30. }
  31. public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
  32. if (Objects.isNull(operation)) {
  33. String content = DataMaskingFunc.ALL_MASK.operation().mask((String) value, null);
  34. gen.writeString(content);
  35. } else {
  36. String content = operation.mask((String) value, null);
  37. gen.writeString(content);
  38. }
  39. }
  40. public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
  41. this.serialize(value, gen, provider);
  42. }
  43. public JsonNode getSchema(SerializerProvider provider) {
  44. return this.createSchemaNode("string", true);
  45. }
  46. public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
  47. this.visitStringFormat(visitor, typeHint);
  48. }
  49. }

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

  1. package com.wkf.workrecord.tools.desensitization;
  2. import com.fasterxml.jackson.databind.introspect.Annotated;
  3. import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
  4. import lombok.extern.slf4j.Slf4j;
  5. /**
  6. * @author wuKeFan
  7. * @date 2023-02-20 09:43:41
  8. */
  9. @Slf4j
  10. public class DataMaskingAnnotationIntroSpector extends NopAnnotationIntrospector {
  11. @Override
  12. public Object findSerializer(Annotated am) {
  13. DataMasking annotation = am.getAnnotation(DataMasking.class);
  14. if (annotation != null) {
  15. return new DataMaskingSerializer(annotation.maskFunc().operation());
  16. }
  17. return null;
  18. }
  19. }

3.4 覆盖 ObjectMapper:

  1. package com.wkf.workrecord.tools.desensitization;
  2. import com.fasterxml.jackson.databind.AnnotationIntrospector;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
  5. import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.context.annotation.Primary;
  9. import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
  10. /**
  11. * 覆盖 ObjectMapper
  12. * @author wuKeFan
  13. * @date 2023-02-20 09:44:35
  14. */
  15. @Configuration(proxyBeanMethods = false)
  16. public class DataMaskConfiguration {
  17. @Configuration(proxyBeanMethods = false)
  18. @ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
  19. static class JacksonObjectMapperConfiguration {
  20. JacksonObjectMapperConfiguration() {
  21. }
  22. @Bean
  23. @Primary
  24. ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
  25. ObjectMapper objectMapper = builder.createXmlMapper(false).build();
  26. AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
  27. AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntroSpector());
  28. objectMapper.setAnnotationIntrospector(newAi);
  29. return objectMapper;
  30. }
  31. }
  32. }

3.5 返回对象加上注解:

  1. package com.wkf.workrecord.tools.desensitization;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. /**
  5. * 需要脱敏的实体类
  6. * @author wuKeFan
  7. * @date 2023-02-20 09:35:52
  8. */
  9. @Data
  10. public class User implements Serializable {
  11. /**
  12. * 主键ID
  13. */
  14. private Long id;
  15. /**
  16. * 姓名
  17. */
  18. @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
  19. private String name;
  20. /**
  21. * 年龄
  22. */
  23. private Integer age;
  24. /**
  25. * 邮箱
  26. */
  27. @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
  28. private String email;
  29. }

4 测试

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

4.1 测试的Controller类DesensitizationController.class如下:

  1. package com.wkf.workrecord.tools.desensitization;
  2. import com.biboheart.brick.model.BhResponseResult;
  3. import com.wkf.workrecord.utils.ResultVOUtils;
  4. import lombok.RequiredArgsConstructor;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RequestMethod;
  8. import org.springframework.web.bind.annotation.RestController;
  9. /**
  10. * 测试接口脱敏测试控制类
  11. * @author wuKeFan
  12. * @date 2022-06-21 17:23
  13. */
  14. @Slf4j
  15. @RestController
  16. @RequiredArgsConstructor
  17. @RequestMapping("/desensitization/")
  18. public class DesensitizationController {
  19. @RequestMapping(value = "test", method = {RequestMethod.GET, RequestMethod.POST})
  20. public BhResponseResult<User> test() {
  21. User user = new User();
  22. user.setAge(1);
  23. user.setEmail("123456789@qq.com");
  24. user.setName("吴名氏");
  25. user.setId(1L);
  26. return ResultVOUtils.success(user);
  27. }
  28. }

4.2 PostMan接口请求,效果符合预期,如图:

6b3ea27396de48f29e2b1139bdfc296a.png

发表评论

表情:
评论列表 (有 0 条评论,88人围观)

还没有评论,来说两句吧...

相关阅读

    相关 SpringBoot实现返回数据脱敏

    介绍 SpringBoot实现返回数据脱敏 有时,敏感数据返回时,需要进行隐藏处理,但是如果一个字段一个字段的进行硬编码处理的话,不仅增加了工作量,而且后期需