Java Jackson

一、背景

实际的业务开发过程中,经常需要对用户的隐私数据进行脱敏处理,所谓脱敏处理其实就是将数据进行混淆隐藏,例如下图,将用户的手机号、地址等数据信息,采用*进行隐藏,以免泄露个人隐私信息。
Jackson序列化时进行数据脱敏处理 - 图1
如果需要脱敏的数据范围很小很小,甚至就是指定的字段,一般的处理方式也很简单,就是写一个隐藏方法即可实现数据脱敏。
Jackson序列化时进行数据脱敏处理 - 图2
如果是需求很少的情况下,采用这种方式实现没太大问题,好维护!
但如果是类似上面那种很多位置的数据,需要分门别类的进行脱敏处理,通过这种简单粗暴的处理,代码似乎就显得不太优雅了。
思考一下,可不可以在数据输出的阶段,进行统一数据脱敏处理,这样就可以省下不少体力活。
说到数据输出,很多同学可能会想到 JSON 序列化。熟悉的 web 系统,就是将数据通过 json 序列化之后展示给前端。
那么问题来了,如何在序列化的时候,进行数据脱敏处理呢?

二、程序实践

2.1、首先添加依赖包

默认的情况下,如果当前项目已经添加了spring-web包或者spring-boot-starter-web包,因为这些jar包已经集成了jackson相关包,因此无需重复依赖。
如果当前项目没有jackson包,可以通过如下方式进行添加相关依赖包。

  1. <!--jackson依赖-->
  2. <dependency>
  3. <groupId>com.fasterxml.jackson.core</groupId>
  4. <artifactId>jackson-core</artifactId>
  5. <version>2.9.8</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.fasterxml.jackson.core</groupId>
  9. <artifactId>jackson-annotations</artifactId>
  10. <version>2.9.8</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>com.fasterxml.jackson.core</groupId>
  14. <artifactId>jackson-databind</artifactId>
  15. <version>2.9.8</version>
  16. </dependency>

2.2、编写脱敏类型枚举类,满足不同场景的处理

  1. public enum SensitiveEnum {
  2. /**
  3. * 中文名
  4. */
  5. CHINESE_NAME,
  6. /**
  7. * 身份证号
  8. */
  9. ID_CARD,
  10. /**
  11. * 座机号
  12. */
  13. FIXED_PHONE,
  14. /**
  15. * 手机号
  16. */
  17. MOBILE_PHONE,
  18. /**
  19. * 地址
  20. */
  21. ADDRESS,
  22. /**
  23. * 电子邮件
  24. */
  25. EMAIL,
  26. /**
  27. * 银行卡
  28. */
  29. BANK_CARD,
  30. /**
  31. * 公司开户银行联号
  32. */
  33. CNAPS_CODE
  34. }

2.3、编写脱敏注解类

  1. import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
  2. import com.fasterxml.jackson.databind.annotation.JsonSerialize;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @JacksonAnnotationsInside
  7. @JsonSerialize(using = SensitiveSerialize.class)
  8. public @interface SensitiveWrapped {
  9. /**
  10. * 脱敏类型
  11. * @return
  12. */
  13. SensitiveEnum value();
  14. }

2.4、编写脱敏序列化类

  1. import com.fasterxml.jackson.core.JsonGenerator;
  2. import com.fasterxml.jackson.databind.BeanProperty;
  3. import com.fasterxml.jackson.databind.JsonMappingException;
  4. import com.fasterxml.jackson.databind.JsonSerializer;
  5. import com.fasterxml.jackson.databind.SerializerProvider;
  6. import com.fasterxml.jackson.databind.ser.ContextualSerializer;
  7. import java.io.IOException;
  8. import java.util.Objects;
  9. public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
  10. /**
  11. * 脱敏类型
  12. */
  13. private SensitiveEnum type;
  14. @Override
  15. public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  16. switch (this.type) {
  17. case CHINESE_NAME: {
  18. jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s));
  19. break;
  20. }
  21. case ID_CARD: {
  22. jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s));
  23. break;
  24. }
  25. case FIXED_PHONE: {
  26. jsonGenerator.writeString(SensitiveInfoUtils.fixedPhone(s));
  27. break;
  28. }
  29. case MOBILE_PHONE: {
  30. jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s));
  31. break;
  32. }
  33. case ADDRESS: {
  34. jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4));
  35. break;
  36. }
  37. case EMAIL: {
  38. jsonGenerator.writeString(SensitiveInfoUtils.email(s));
  39. break;
  40. }
  41. case BANK_CARD: {
  42. jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s));
  43. break;
  44. }
  45. case CNAPS_CODE: {
  46. jsonGenerator.writeString(SensitiveInfoUtils.cnapsCode(s));
  47. break;
  48. }
  49. }
  50. }
  51. @Override
  52. public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
  53. // 为空直接跳过
  54. if (beanProperty != null) {
  55. // 非 String 类直接跳过
  56. if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
  57. SensitiveWrapped sensitiveWrapped = beanProperty.getAnnotation(SensitiveWrapped.class);
  58. if (sensitiveWrapped == null) {
  59. sensitiveWrapped = beanProperty.getContextAnnotation(SensitiveWrapped.class);
  60. }
  61. if (sensitiveWrapped != null) {
  62. // 如果能得到注解,就将注解的 value 传入 SensitiveSerialize
  63. return new SensitiveSerialize(sensitiveWrapped.value());
  64. }
  65. }
  66. return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
  67. }
  68. return serializerProvider.findNullValueSerializer(beanProperty);
  69. }
  70. public SensitiveSerialize() {}
  71. public SensitiveSerialize(final SensitiveEnum type) {
  72. this.type = type;
  73. }
  74. }

其中createContextual的作用是通过字段已知的上下文信息定制JsonSerializer对象。

2.4、编写脱敏工具类

  1. import org.apache.commons.lang3.StringUtils;
  2. public class SensitiveInfoUtils {
  3. /**
  4. * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
  5. */
  6. public static String chineseName(final String fullName) {
  7. if (StringUtils.isBlank(fullName)) {
  8. return "";
  9. }
  10. final String name = StringUtils.left(fullName, 1);
  11. return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
  12. }
  13. /**
  14. * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
  15. */
  16. public static String chineseName(final String familyName, final String givenName) {
  17. if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) {
  18. return "";
  19. }
  20. return chineseName(familyName + givenName);
  21. }
  22. /**
  23. * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:420**********5762>
  24. */
  25. public static String idCardNum(final String id) {
  26. if (StringUtils.isBlank(id)) {
  27. return "";
  28. }
  29. return StringUtils.left(id, 3).concat(StringUtils
  30. .removeStart(StringUtils.leftPad(StringUtils.right(id, 4), StringUtils.length(id), "*"),
  31. "***"));
  32. }
  33. /**
  34. * [固定电话] 后四位,其他隐藏<例子:****1234>
  35. */
  36. public static String fixedPhone(final String num) {
  37. if (StringUtils.isBlank(num)) {
  38. return "";
  39. }
  40. return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
  41. }
  42. /**
  43. * [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>
  44. */
  45. public static String mobilePhone(final String num) {
  46. if (StringUtils.isBlank(num)) {
  47. return "";
  48. }
  49. return StringUtils.left(num, 3).concat(StringUtils
  50. .removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"),
  51. "***"));
  52. }
  53. /**
  54. * [地址] 只显示到地区,不显示详细地址;要对个人信息增强保护<例子:北京市海淀区****>
  55. *
  56. * @param sensitiveSize 敏感信息长度
  57. */
  58. public static String address(final String address, final int sensitiveSize) {
  59. if (StringUtils.isBlank(address)) {
  60. return "";
  61. }
  62. final int length = StringUtils.length(address);
  63. return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
  64. }
  65. /**
  66. * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com>
  67. */
  68. public static String email(final String email) {
  69. if (StringUtils.isBlank(email)) {
  70. return "";
  71. }
  72. final int index = StringUtils.indexOf(email, "@");
  73. if (index <= 1) {
  74. return email;
  75. } else {
  76. return StringUtils.rightPad(StringUtils.left(email, 1), index, "*")
  77. .concat(StringUtils.mid(email, index, StringUtils.length(email)));
  78. }
  79. }
  80. /**
  81. * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>
  82. */
  83. public static String bankCard(final String cardNum) {
  84. if (StringUtils.isBlank(cardNum)) {
  85. return "";
  86. }
  87. return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(
  88. StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"),
  89. "******"));
  90. }
  91. /**
  92. * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号<例子:12********>
  93. */
  94. public static String cnapsCode(final String code) {
  95. if (StringUtils.isBlank(code)) {
  96. return "";
  97. }
  98. return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*");
  99. }
  100. }

2.5、编写测试实体类

最后,编写一个实体类UserEntity,看看转换后的效果如何?

  1. public class UserEntity {
  2. /**
  3. * 用户ID
  4. */
  5. private Long userId;
  6. /**
  7. * 用户姓名
  8. */
  9. private String name;
  10. /**
  11. * 手机号
  12. */
  13. @SensitiveWrapped(SensitiveEnum.MOBILE_PHONE)
  14. private String mobile;
  15. /**
  16. * 身份证号码
  17. */
  18. @SensitiveWrapped(SensitiveEnum.ID_CARD)
  19. private String idCard;
  20. /**
  21. * 年龄
  22. */
  23. private String sex;
  24. /**
  25. * 性别
  26. */
  27. private int age;
  28. //省略get、set...
  29. }

测试程序如下:

  1. public class SensitiveDemo {
  2. public static void main(String[] args) throws JsonProcessingException {
  3. UserEntity userEntity = new UserEntity();
  4. userEntity.setUserId(1l);
  5. userEntity.setName("张三");
  6. userEntity.setMobile("18000000001");
  7. userEntity.setIdCard("420117200001011000008888");
  8. userEntity.setAge(20);
  9. userEntity.setSex("男");
  10. //通过jackson方式,将对象序列化成json字符串
  11. ObjectMapper objectMapper = new ObjectMapper();
  12. System.out.println(objectMapper.writeValueAsString(userEntity));
  13. }
  14. }

结果如下:

  1. {"userId":1,"name":"张三","mobile":"180****0001","idCard":"420*****************8888","sex":"男","age":20}

很清晰的看到,转换结果成功!
如果当前的项目是基于SpringMVC框架进行开发的,那么在对象返回的时候,框架会自动采用jackson框架进行序列化。

  1. @RequestMapping("/hello")
  2. public UserEntity hello() {
  3. UserEntity userEntity = new UserEntity();
  4. userEntity.setUserId(1l);
  5. userEntity.setName("张三");
  6. userEntity.setMobile("18000000001");
  7. userEntity.setIdCard("420117200001011000008888");
  8. userEntity.setAge(20);
  9. userEntity.setSex("男");
  10. return userEntity;
  11. }

请求网页http://127.0.0.1:8080/hello,结果如下:
Jackson序列化时进行数据脱敏处理 - 图3

三、小结

在实际的业务场景开发中,采用注解方式进行全局数据脱敏处理,可以有效的解决敏感数据隐私泄露的问题。