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;
    }

访问接口:

相关推荐
MacroZheng1 小时前
还在用WebSocket实现即时通讯?试试MQTT吧,真香!
java·spring boot·后端
midsummer_woo2 小时前
基于springboot的IT技术交流和分享平台的设计与实现(源码+论文)
java·spring boot·后端
别惹CC3 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
人工智能·spring boot·spring
柯南二号4 小时前
【Java后端】Spring Boot 集成 MyBatis-Plus 全攻略
java·spring boot·mybatis
javachen__5 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
IT毕设实战小研11 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
一只爱撸猫的程序猿12 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋13 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
武昌库里写JAVA15 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习
Pitayafruit16 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
spring boot·后端·llm