Jackson 2.x 系列【30】Spring Boot 集成之数据脱敏

有道无术,术尚可求,有术无道,止于术。

本系列Jackson 版本 2.17.0

本系列Spring Boot 版本 3.2.4

源码地址:https://gitee.com/pearl-organization/study-jaskson-demo

文章目录

    • [1. 概述](#1. 概述)
    • [2. 实现思路](#2. 实现思路)
    • [3. 案例演示](#3. 案例演示)
      • [3.1 脱敏规则](#3.1 脱敏规则)
      • [3.2 自定义注解](#3.2 自定义注解)
      • [3.3 自定义序列化器](#3.3 自定义序列化器)
      • [3.4 测试](#3.4 测试)

1. 概述

数据脱敏指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在开发、测试和其它非生产环境以及外包环境中安全地使用脱敏后的真实数据集。例如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。

按照脱敏方式分为:

  • 静态数据脱敏:按照脱敏规则一次性完成大批量数据的变形转换处理
  • 动态数据脱敏:按照脱敏规则对于外部申请访问的数据进行即时处理并返回脱敏后的结果

随着 《网络安全法》 的颁布施行,对个人隐私数据的保护已经上升到法律层面。传统的应用系统普遍缺少对个人隐私数据的保护措施。数据脱敏,可实现在不需要对生产数据库中的数据进行任何改变的情况下,依据用户定义的脱敏规则,对生产数据库返回的数据进行专门的加密、遮盖和替换,确保生产环境的敏感数据能够得到保护。

2. 实现思路

一般可以在以下两种时机进行脱敏操作:

  • 数据库查询返回对象时
  • 序列化返回浏览器时

Spring Web默认使用Jackson作为HTTP消息转换器的Json处理框架,那么可以自定义Jackson的序列化器对字段进行脱敏处理。

3. 案例演示

演示需求:

  • 身份证号显示为:43************6363
  • 手机号显示为:135****8888

3.1 脱敏规则

常用的脱敏规则有:

  • MD5:直接使用MD5计算
  • 遮盖脱敏:使用特殊字符遮盖,比如*
  • 替换脱敏:使用码表进行替换

演示需求中需要使用遮盖脱敏 规则,将部分信息使用*进行遮盖。

3.2 自定义注解

参考Apache ShardingSphere的遮盖自 XY脱敏算法,定义一个脱敏注解,指定开始、结束位置 ,之间的字符都会使用指定的字符进行遮盖。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.ANNOTATION_TYPE})
@JacksonAnnotationsInside
@JsonSerialize(using = MaskFromXToYJsonSerializer.class)
public @interface MaskFromXToYMask {

    /**
     * 起始位置 (从 0 开始计数)
     */
    int from();

    /**
     * 结束位置(从 0 开始计数)
     */
    int to();

    /**
     * 替换字符,默认*
     */
    String replaceChar() default "*";
}

定义身份证脱敏注解,遮盖2-13位置的字符:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@MaskFromXToYMask(from =2 ,to =13)
public @interface MaskIdNum {
}

定义手机号脱敏注解,遮盖3-6位置的字符:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@MaskFromXToYMask(from =3 ,to =6)
public @interface MaskPhone {
}

3.3 自定义序列化器

自定义序列化器MaskFromXToYJsonSerializer,获取字段上的脱敏注解,序列化时进行遮盖写出:

java 复制代码
public class MaskFromXToYJsonSerializer extends StdSerializer<String> implements ContextualSerializer {

    private int from;

    private int to;

    private String replaceChar;

    public MaskFromXToYJsonSerializer(Class<String> t, int from, int to, String replaceChar) {
        super(t);
        this.from = from;
        this.to = to;
        this.replaceChar = replaceChar;
    }

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

    /**
     * 序列化
     */
    @Override
    public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        // 1. 校验
        if (this.from >= this.to) {
            throw new RuntimeException("起始位置不能大于等于结束位置");
        }
        if (StrUtil.isEmpty(this.replaceChar)) {
            throw new RuntimeException("替换字符不能为空");
        }
        // 2. 序列化
        if (StrUtil.isEmpty(str)) {
            // 为空
            jsonGenerator.writeString("");
        } else if (str.length() <= this.from) {
            // 字符长度小于开始位置
            jsonGenerator.writeString(str);
        } else {
            // 脱敏写出
            char[] chars = str.toCharArray();
            int i = this.from;
            for (int minLength = Math.min(this.to, chars.length - 1); i <= minLength; ++i) {
                chars[i] = this.replaceChar.charAt(0);
            }
            jsonGenerator.writeString(new String(chars));
        }
    }

    /**
     * 根据字段注解,返回序列化器,仅在在第一次序列化时调用
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializers, BeanProperty property) throws JsonMappingException {
        // 1. 属性为 Null
        if (property == null) {
            return serializers.findNullValueSerializer(null);
        }
        // 2. 属性的类型
        Class<?> rawClass = property.getType().getRawClass();
        if (CharSequence.class.isAssignableFrom(rawClass)) {
            // 3. 字符串类型,返回脱敏序列化器
            MaskFromXToYMask annotation = property.getAnnotation(MaskFromXToYMask.class);
            if (annotation == null) {
                annotation = property.getContextAnnotation(MaskFromXToYMask.class);
            }
            if (annotation != null) {
                return new MaskFromXToYJsonSerializer(String.class, annotation.from(), annotation.to(), annotation.replaceChar());
            }

        }
        return serializers.findValueSerializer(property.getType(), property);
    }
}

3.4 测试

定义一个用户对象,添加脱敏注解:

java 复制代码
@Data
@ToString
public class UserVO implements Serializable {

    Long id;

    String username;

    List<String> roleList;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    Date birthday;

    @MaskIdNum
    String idNum;

    @MaskPhone
    String phone;
}

常见一个访问接口:

java 复制代码
    @RequestMapping("/test")
    public UserVO test() {
        UserVO userVO = new UserVO();
        userVO.setId(1699657986705854464L);
        userVO.setUsername("jack");
        userVO.setBirthday(new Date());
        userVO.setIdNum("430852195602056363");
        userVO.setPhone("13536238888");
        List<String> roleList = new ArrayList<>();
        roleList.add("管理员");
        roleList.add("经理");
        userVO.setRoleList(roleList);
        return userVO;
    }

访问接口:

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
AskHarries6 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion7 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil78 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
星河梦瑾9 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
计算机学长felix10 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴10 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲10 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
nbsaas-boot10 小时前
探索 JSON 数据在关系型数据库中的应用:MySQL 与 SQL Server 的对比
数据库·mysql·json
撒呼呼11 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot