使用Jackson掩盖JSON字段

6

我试图在使用Jackson进行序列化时掩盖敏感数据。

我尝试使用@JsonSerialize和自定义注释@Mask。

Mask.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Mask {
  String value() default "XXX-DEFAULT MASK FORMAT-XXX";
}

Employee.java

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.util.Map;

public class Employee {

  @Mask(value = "*** The value of this attribute is masked for security reason ***")
  @JsonSerialize(using = MaskStringValueSerializer.class)
  protected String name;

  @Mask
  @JsonSerialize(using = MaskStringValueSerializer.class)
  protected String empId;

  @JsonSerialize(using = MaskMapStringValueSerializer.class)
  protected Map<Category, String> categoryMap;

  public Employee() {
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getEmpId() {
    return empId;
  }

  public void setEmpId(String empId) {
    this.empId = empId;
  }

  public Map<Category, String> getCategoryMap() {
    return categoryMap;
  }

  public void setCategoryMap(Map<Category, String> categoryMap) {
    this.categoryMap = categoryMap;
  }
}

Category.java

public enum Category {
  @Mask
  CATEGORY1,
  @Mask(value = "*** This value of this attribute is masked for security reason ***")
  CATEGORY2,
  CATEGORY3;
}

MaskMapStringValueSerializer.java

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.util.Map;

public class MaskMapStringValueSerializer extends JsonSerializer<Map<Category, String>> {

  @Override
  public void serialize(Map<Category, String> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    jsonGenerator.writeStartObject();

    for (Category key : map.keySet()) {
      Mask annot = null;
      try {
        annot = key.getClass().getField(key.name()).getAnnotation(Mask.class);
      } catch (NoSuchFieldException e) {
        e.printStackTrace();
      }

      if (annot != null) {
        jsonGenerator.writeStringField(((Category) key).name(), annot.value());
      } else {
        jsonGenerator.writeObjectField(((Category) key).name(), map.get(key));
      }
    }

    jsonGenerator.writeEndObject();

  }
}

MaskStringValueSerializer.java

import com.fasterxml.jackson.core.JsonGenerator;
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.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;

public class MaskStringValueSerializer extends StdSerializer<String> implements ContextualSerializer {
  private Mask annot;

  public MaskStringValueSerializer() {
    super(String.class);
  }

  public MaskStringValueSerializer(Mask logMaskAnnotation) {
    super(String.class);
    this.annot = logMaskAnnotation;
  }

  public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    if (annot != null && s != null && !s.isEmpty()) {
      jsonGenerator.writeString(annot.value());
    } else {
      jsonGenerator.writeString(s);
    }
  }

  public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
    Mask annot = null;
    if (beanProperty != null) {
      annot = beanProperty.getAnnotation(Mask.class);
    }
    return new MaskStringValueSerializer(annot);

  }
}

MaskValueTest.java

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class MaskValueTest {


  public static void main(String args[]) throws Exception{
    Employee employee = new Employee();

    employee.setName("John Doe");
    employee.setEmpId("1234567890");
    Map<Category, String> catMap = new HashMap<>();
    catMap.put(Category.CATEGORY1, "CATEGORY1");
    catMap.put(Category.CATEGORY2, "CATEGORY2");
    catMap.put(Category.CATEGORY3, "CATEGORY3");
    employee.setCategoryMap(catMap);

    ObjectMapper mapper = new ObjectMapper();
    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
  }
}

输出 -

{
  "name" : "*** The value of this attribute is masked for security reason ***",
  "empId" : "XXX-DEFAULT MASK FORMAT-XXX",
  "categoryMap" : {
    "CATEGORY1" : "XXX-DEFAULT MASK FORMAT-XXX",
    "CATEGORY2" : "*** The value of this attribute is masked for security reason ***",
    "CATEGORY3" : "CATEGORY3"
  }
}
  • 结果符合预期,但这似乎是静态掩码。
  • 本意是只在需要时进行掩盖,例如在日志打印时,所有这些敏感数据都应该被掩盖。
  • 如果我必须将此JSON发送到文档索引中,其中的值应保持不变,则此实现失败。

我正在寻找一种基于注释的解决方案,其中我可以使用初始化为JsonSerializers的2个不同的ObjectMapper实例。

4个回答

4
这是一种实现Andreas建议的方法: 创建一个类MaskAnnotationIntrospector,它继承自JacksonAnnotationIntrospector并覆盖其findSerializer方法,代码如下:
public class MaskAnnotationIntrospector extends JacksonAnnotationIntrospector {

    @Override
    public Object findSerializer(Annotated am) {
        Mask annotation = am.getAnnotation(Mask.class);
        if (annotation != null)
            return MaskingSerializer.class;

        return super.findSerializer(am);
    }
}

因此,您可以拥有两个ObjectMapper实例。将MaskAnnotationIntrospector添加到您想要屏蔽的实例中(例如,用于日志记录):
mapper.setAnnotationIntrospector(new MaskAnnotationIntrospector());
< p >另一个MaskAnnotationIntrospector没有设置的实例,在序列化期间不会掩盖任何内容。

P.S. MaskAnnotationIntrospector可以从JacksonAnnotationIntrospectorNopAnnotationIntrospector中扩展,但后者不提供findSerializer方法的任何实现,并且调用super.findSerializer(am)只会返回null,因此其他Jackson注释(例如@JsonIgnore)被丢弃,但使用前者可以解决这个问题。


2

不必使用MaskStringValueSerializer.java,您可以创建一个模块来捆绑序列化器,并在需要时向objectmapper注册该模块,这最终将允许您拥有两个不同的objectmapper实例。

创建一个模块来捆绑序列化器

public class MaskingModule extends SimpleModule {
    private static final String NAME = "CustomIntervalModule";
    private static final VersionUtil VERSION_UTIL = new VersionUtil() {};

    public MaskingModule() {
      super(NAME, VERSION_UTIL.version());
      addSerializer(MyBean.class, new MaskMapStringValueSerializer());
    }
}

在ObjectMapper中注册该模块并使用它。
 ObjectMapper objectMapper = new ObjectMapper().registerModule(new MaskingModule());
 System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));

您可以扩展对象映射器并注册模块以使用它。
public class CustomObjectMapper extends ObjectMapper {
    public CustomObjectMapper() {
      registerModule(new MaskingModule());
    }
  }


 CustomObjectMapper customObjectMapper = new CustomObjectMapper ();
 System.out.println(customObjectMapper .writerWithDefaultPrettyPrinter().writeValueAsString(employee));

为什么要创建ObjectMapper的子类?让调用者执行registerModule()调用。 - Andreas
添加了让他选择他想要的功能 @Andreas - Srinivasan Sekar
@SrinivasanSekar,感谢您的建议。您的想法听起来不错。我也在尝试将串行化器绑定到任何特定的bean类型上。我的目标是创建一个通用的串行化器,可以处理任何字符串值,无论是类型内的属性、映射中的字符串值还是类型内的整个类型。 - Samar

1
去掉@JsonSerialize注释,并将如何处理@Mask注释的逻辑放入Module中,例如,让它添加AnnotationIntrospector
现在,您可以选择是否调用registerModule(Module module)
至于编写模块,我会让您自行决定。如果有任何问题,请提出另一个问题。

如何在使用相同注释时为两种不同类型执行此操作?例如,在上面的示例中,我在**namecategoryMap**上都有注释。 它们都需要针对相同注释类型的2个不同序列化程序。 - Samar
实现findPropertyContentTypeResolverfindPropertyTypeResolver - Andreas

-1
为什么不使用两个参数,一个用于原始值,另一个用于掩码值。例如,在这种情况下,您可以使用String name和String maskedName。然后,对于日志记录,您可以使用掩码值。

两个参数在哪里?它会如何知道使用哪一个? - Andreas
将您的掩码注释添加到maskedName字段中。当您将对象传递给记录器时,通常会执行toString方法。覆盖toString方法并从中删除原始名称变量。然后在记录日志时,它将仅打印掩码值。对于其他内容,您可以从对象中获取原始名称值。不要掩盖原始值。 - Vimukthi
似乎您错过了问题的整个部分,即OP正在使用ObjectMapper生成(掩码)JSON以记录日志,但也正在使用ObjectMapper生成(未掩码)JSON以进行文档索引。问题是如何做到这一点,即如何使ObjectMapper仅在某些情况下进行掩码。 - Andreas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接