springboot学习(七十八) springboot中通过自定义注解实现数据脱敏的功能

news2024/11/29 0:52:07

文章目录

  • 前言
  • 一、引入hutools工具类
  • 二、定义常用需要脱敏的数据类型的枚举
  • 三、定义脱敏方式枚举
  • 四、自定义脱敏的注解
  • 五、自定义Jackson的序列化方式
  • 六、使用
  • 七、脱敏效果


前言

对于某些接口返回的信息,涉及到敏感数据的必须进行脱敏操作,例如银行卡号、身份证号、手机号等,脱敏方式有多种方式。可以修改SQL语句,也可以写硬代码,也可以修改JSON序列化,这里介绍通过修改Jackson序列化方式实现数据脱敏。


一、引入hutools工具类

maven:

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
</dependency>

gradle:

// https://mvnrepository.com/artifact/cn.hutool/hutool-all
implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.5'

二、定义常用需要脱敏的数据类型的枚举

其中 OTHER类型为自定义类型,需在后面自定义脱敏的长度等。

package com.iscas.authentication.model.enums;

import lombok.Getter;

/**
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/5 8:44
 * @since jdk1.8
 */
@Getter
public enum PrivacyTypeEnum {

    /**
     * 中文名
     * */
    CHINESE_NAME,

    /**
     * 固话
     * */
    FIXED_PHONE,

    /**
     * 手机号
     * */
     MOBILE_PHONE,

    /**
     * 住址
     * */
    ADDRESS,

    /**
     * 密码
     * */
    PASSWORD,

    /**
     * 银行卡号
     * */
    BANK_CARD,

    /**
     * 邮箱
     * */
    EMAIL,

    /**
     * 身份证
     * */
    ID_CARD,

    /**
     * 其他类型
     * */
    OTHER;
}

三、定义脱敏方式枚举

其中,DEFAULT类型时,需要数据类型为上一步枚举中除OTHER外的已确定的类型,NONE表示不做脱敏,其他类型为注释的意思。

package com.iscas.authentication.model.enums;

/**
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/5 9:11
 * @since jdk1.8
 */
public enum DesensitizationTypeEnum {

    /**
     * 默认方式
     * */
    DEFAULT,

    /**
     * 头部脱敏
     * */
    HEAD,

    /**
     * 尾部脱敏
     * */
    TAIL,

    /**
     * 中间脱敏
     * */
    MIDDLE,

    /**
     * 头尾脱敏
     * */
    HEAD_TAIL,

    /**
     * 全部脱敏
     * */
    ALL,

    /**
     * 不脱敏,相当于没打这个注解
     * */
    NONE;

}

四、自定义脱敏的注解

其中,mode默认为DEFAULT,此时只需要设置dataType的类型为除OTHER外的确定类型即可,当mode不是DEFAULT或NONE时,根据不同的类型,headNoMaskLen等长度属性需要设置,见上面的注释的字面意思。

package com.iscas.authentication.annotation;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.iscas.authentication.model.enums.DesensitizationTypeEnum;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import com.iscas.authentication.service.DesensitizationSerializer;

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

/**
 * 脱敏注解
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/5 8:48
 * @since jdk1.8
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerializer.class)
public @interface Desensitization {
    /**
     * 脱敏的隐私数据类型
     */
    PrivacyTypeEnum dataType();

    /**
     * 脱敏方式,默认方式不需要定义下面脱敏长度等信息,根据脱敏的隐私数据类型自动脱敏
     */
    DesensitizationTypeEnum mode() default DesensitizationTypeEnum.DEFAULT;

    /**
     * 尾部不脱敏的长度,当mode为HEAD或MIDDLE时使用
     */
    int tailNoMaskLen() default 1;

    /**
     * 头部不脱敏的长度,当mode为TAIL或MIDDLE时使用
     */
    int headNoMaskLen() default 1;

    /**
     * 中间不脱敏的长度,当mode为HEAD_TAIL时使用
     */
    int middleNoMaskLen() default 1;

    /**
     * 打码
     */
    char maskCode() default '*';

}

五、自定义Jackson的序列化方式

package com.iscas.authentication.service;

import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
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.iscas.authentication.annotation.Desensitization;
import com.iscas.authentication.model.enums.DesensitizationTypeEnum;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.util.Objects;

/**
 * 脱敏序列化类
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/5 9:24
 * @since jdk1.8
 */
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer {

    private Desensitization desensitization;

    @Override
    public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(desensitize(s));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
                if (desensitization == null) {
                    desensitization = beanProperty.getContextAnnotation(Desensitization.class);
                }
                if (desensitization != null) {
                    return new DesensitizationSerializer(desensitization);
                }
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(null);
    }

    /**
     * 脱敏处理
     * */
    private String desensitize(String s) {
        if (StrUtil.isNotBlank(s)) {
            PrivacyTypeEnum dataType = desensitization.dataType();
            DesensitizationTypeEnum mode = desensitization.mode();
            switch (mode) {
                case DEFAULT:
                    // 默认方式,根据dataType自动选择脱敏方式
                    s = autoDesensitize(s, dataType);
                    break;
                case HEAD:
                    // 头部脱敏
                    s = headDesensitize(s);
                    break;
                case TAIL:
                    // 尾部脱敏
                    s = tailDesensitize(s);
                    break;
                case MIDDLE:
                    s = middleDesensitize(s);
                    break;
                case HEAD_TAIL:
                    s = headTailDesensitize(s);
                    break;
                case ALL:
                    s = allDesensitize(s);
                    break;
                case NONE:
                    // 不做脱敏
                    break;
                default:
            }
        }
        return s;
    }

    /**
     * 全部脱敏
     * */
    private String allDesensitize(String s) {
        return String.valueOf(desensitization.maskCode()).repeat(s.length());
    }

    /**
    * 头尾脱敏
    * */
    private String headTailDesensitize(String s) {
        int middleNoMaskLen = desensitization.middleNoMaskLen();
        if (middleNoMaskLen >= s.length()) {
            // 如果中间不脱敏的长度大于等于字符串的长度,不进行脱敏
            return s;
        }
        int len = s.length() - middleNoMaskLen;
        // 头部脱敏
        int headStart = 0;
        int headEnd = len / 2;
        s = StrUtil.replace(s, headStart, headEnd, desensitization.maskCode());
        // 尾部脱敏
        int tailStart = s.length() - (len - len / 2);
        int tailEnd = s.length();
        return StrUtil.replace(s, tailStart, tailEnd, desensitization.maskCode());
    }

    /**
     * 中间脱敏
     * */
    private String middleDesensitize(String s) {
        int headNoMaskLen = desensitization.headNoMaskLen();
        int tailNoMaskLen = desensitization.tailNoMaskLen();
        if (headNoMaskLen + tailNoMaskLen >= s.length()) {
            // 如果头部不脱敏的长度+尾部不脱敏长度 大于等于字符串的长度,不进行脱敏
            return s;
        }
        int start = headNoMaskLen;
        int end = s.length() - tailNoMaskLen;
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }

    /**
     * 尾部脱敏
     * */
    private String tailDesensitize(String s) {
        int headNoMaskLen = desensitization.headNoMaskLen();
        if (headNoMaskLen >= s.length()) {
            // 如果头部不脱敏的长度大于等于字符串的长度,不进行脱敏
            return s;
        }
        int start = headNoMaskLen;
        int end = s.length();
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }

    /**
     * 头部脱敏
     * */
    private String headDesensitize(String s) {
        int tailNoMaskLen = desensitization.tailNoMaskLen();
        if (tailNoMaskLen >= s.length()) {
            // 如果尾部不脱敏的长度大于等于字符串的长度,不进行脱敏
            return s;
        }
        int start = 0;
        int end = s.length() - tailNoMaskLen;
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }

    public static void main(String[] args) {
        System.out.println(StrUtil.replace("231085198901091813", 2, -10, '#'));
    }

    /**
     * 根据数据类型自动脱敏
     * */
    private String autoDesensitize(String s, PrivacyTypeEnum dataType) {
        switch (dataType) {
            case CHINESE_NAME:
                s = DesensitizedUtil.chineseName(s);
                break;
            case FIXED_PHONE:
                s = DesensitizedUtil.fixedPhone(s);
                break;
            case MOBILE_PHONE:
                s = DesensitizedUtil.mobilePhone(s);
                break;
            case ADDRESS:
                s = DesensitizedUtil.address(s, 8);
                break;
            case PASSWORD:
                s = DesensitizedUtil.password(s);
                break;
            case BANK_CARD:
                s = DesensitizedUtil.bankCard(s);
                break;
            case EMAIL:
                s = DesensitizedUtil.email(s);
                break;
            case ID_CARD:
                s = DesensitizedUtil.idCardNum(s, 1, 2);
                break;
            case OTHER:
                // 其他类型的不支持以默认方式脱敏,直接返回
                break;
            default:
        }
        return s;
    }
}

六、使用

如下,在tel、password、email上添加了@Desensitization注解

package com.iscas.authentication.model.sys;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.iscas.authentication.annotation.Desensitization;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import com.iscas.templet.annotation.table.TbField;
import com.iscas.templet.annotation.table.TbFieldRule;
import com.iscas.templet.annotation.table.TbSetting;
import com.iscas.templet.view.table.TableFieldType;
import com.iscas.templet.view.table.TableSearchType;
import com.iscas.templet.view.table.TableViewType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;

import java.util.List;

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2022/3/11 21:23
 * @since jdk11
 */
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(title = "用户")
@TableName(value = "oauth_sys_user")
@Accessors(chain = true)
@TbSetting(title = "用户", checkbox = true, viewType = TableViewType.multi)
public class User extends BaseEntity {

    @TableId(type = IdType.AUTO)
    @Schema(title = "id")
    @TbField(field = "id", header = "id",
            type = TableFieldType.text, hidden = true)
    private Integer id;

    @Schema(title = "用户名")
    @TbField(field = "name", header = "名称", search = true, searchType = TableSearchType.like,
            type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, distinct = true, desc = "用户名不能为空,且长度介于2-20个字符之间"))
    private String name;

    @Schema(title = "密码")
    @TbField(field = "password", header = "密码", hidden = true, editable = false,
            type = TableFieldType.text)
    @Desensitization(dataType = PrivacyTypeEnum.PASSWORD)
    private String password;

    @Schema(title = "type")
    @TbField(field = "type", header = "用户类型", search = true, searchType = TableSearchType.exact,
            type = TableFieldType.select, option = "[{\"label\":\"正常用户\",\"value\":\"1\"},{\"label\":\"战位IP用户\",\"value\":\"2\"}]")
    private String type;

    @Schema(title = "status")
    @TbField(field = "status", header = "状态", search = true, searchType = TableSearchType.exact,
            type = TableFieldType.select, option = "[{\"label\":\"正常\",\"value\":\"1\"},{\"label\":\"禁用\",\"value\":\"0\"}]")
    private String status;

    @Schema(title = "真实姓名")
    @TbField(field = "realName", header = "真实姓名",
            type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, desc = "真实姓名不能为空,且长度介于2-20个字符之间"))
    private String realName;

    @Schema(title = "电话号码")
    @TbField(field = "tel", header = "电话号码",
            type = TableFieldType.text, rule=@TbFieldRule(reg = "^(13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\\d{8}$", desc = "电话号码需符规则"))
    @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE)
    private String tel;

    @Schema(title = "邮箱")
    @TbField(field = "email", header = "邮箱",
            type = TableFieldType.text, rule=@TbFieldRule(reg = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", desc = "邮箱需符规则"))
    @Desensitization(dataType = PrivacyTypeEnum.EMAIL)
    private String email;

    @Schema(title = "部门")
    @TbField(field = "orgIds", header = "部门",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/orgs/combobox/tree?status=1")
    @TableField(exist = false)
    private List<Integer> orgIds;

    @Schema(title = "角色")
    @TbField(field = "roleIds", header = "角色",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/roles/combobox?status=1")
    @TableField(exist = false)
    private List<Integer> roleIds;

    @Schema(title = "岗位")
    @TbField(field = "postIds", header = "岗位",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/posts/combobox?status=1")
    @TableField(exist = false)
    private List<Integer> postIds;

}

七、脱敏效果

下面是一个查询接口返回带User实体的结果:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/142572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

带你了解ssh服务过程

远程连接服务 1、什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机以取得可操作主机接口&#xff08;shell&#xff09;&#xff0c;而登录后的操作感觉就像是坐在系统前面一样。 2、远程连接服务器的功…

【C++】函数重载的使用及原理

概述 在学校里&#xff0c;我们都会有班里同学被起外号的经历&#xff0c;而且同一个人可能还会有好几个外号。 在自然语言中&#xff0c;一个词可以有多重含义&#xff0c;人们可以通过上下文来判断该词真实的含义&#xff0c;即该词被重载了。 目录 概述 什么是函数重载 …

项目管理:如何制作项目进度计划表?

项目进度管理是根据项目目标&#xff0c;编制合理的进度计划&#xff0c;并在项目推进过程中随时检查项目执行情况。 项目进度管理的目的就是为了实现最优工期&#xff0c;多快好省地完成任务。 而甘特图&#xff0c;就是用表格图形的方式来展示项目的进展&#xff0c;是一个比…

赛狐ERP:优秀的亚马逊运营具备的五项能力!

我们都知道&#xff0c;亚马逊运营是整个店铺的主导&#xff0c;很大程度上会影响着一个店铺经营的好坏&#xff0c;那么一个好的亚马逊运营&#xff0c;应该具备哪些能力呢&#xff1f;今天赛狐ERP就来给和大家聊一聊&#xff0c;希望对各位亚马逊运营们会有启发&#xff01;1…

ORB-SLAM2 --- LocalMapping::Run 局部建图线程解析

目录 一、线程作用 二、局部建图线程主要流程 三、局部建图线程主函数 四、调用函数解析 4.1 设置"允许接受关键帧"的状态标志LocalMapping::SetAcceptKeyFrames函数解析 4.2 查看列表中是否有等待被插入的关键帧LocalMapping::CheckNewKeyFrames函数 4.3 …

十分钟学会在linux上部署chrony服务器(再见 NTP,是时候拥抱下一代时间同步服务 Chrony 了)

chrony服务器 Chrony 相较于 NTPD 服务的优势 安装与配置&#xff08;Chrony的配置文件是/etc/chrony.conf&#xff09; 同步网络时间服务器 设置开机启动&#xff0c;重启服务 chronyc sources 输出结果解析 练习 实验模型图如下 实验a如下 实验b如下 再见 NTP&#x…

中国手机市场全面衰退,连苹果也未能幸免,大跌近三成

CINNO公布了11月份国内手机市场的数据&#xff0c;数据显示2022年11月份中国市场的手机出货量同比下滑21.7%&#xff0c;在整体大环境出现销量下滑的情况下&#xff0c;此前曾持续逆势增长的苹果也顶不住了&#xff0c;苹果在中国市场的出货量也出现了下滑的势头。数据显示2022…

06-Alibaba Nacos注册中心源码剖析

Nacos&Ribbon&Feign核心微服务架构图 架构原理 1、微服务系统在启动时将自己注册到服务注册中心&#xff0c;同时外发布 Http 接口供其它系统调用(一般都是基于SpringMVC) 2、服务消费者基于 Feign 调用服务提供者对外发布的接口&#xff0c;先对调用的本地接口加上注…

JS继承有哪些,你能否手写其中一两种呢?

引言 JS系列暂定 27 篇&#xff0c;从基础&#xff0c;到原型&#xff0c;到异步&#xff0c;到设计模式&#xff0c;到架构模式等&#xff0c; 本篇是 JS系列中第 3 篇&#xff0c;文章主讲 JS 继承&#xff0c;包括原型链继承、构造函数继承、组合继承、寄生组合继承、原型…

前端vue项目发送请求不携带cookie(vue.config.js和nginx反向代理)

一、本地环境——使用vue.config.js配置了跨域代理本来发现问题&#xff0c;是因为后台记录到接收到的sessionId一直在变化&#xff0c;导致需要在同一个sessionId下处理的逻辑无法实现。一开始以为是前后端分离跨域导致的&#xff0c;网上给出了解决方案&#xff1a;main.js中…

线程同步的实现

线程同步 同步就是协同步调&#xff0c;按预定的先后次序进行运行。如:你说完&#xff0c;我再说。 "同"字从字面上容易理解为一起动作 其实不是&#xff0c;"同"字应是指协同、协助、互相配合。 如进程、线程同步&#xff0c;可理解为进程或线程A和B一…

USB子系统简述

引子&#xff1a;关于 lsusb 命令 lsusb 列出系统中所有的USB设备&#xff1a; Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hubBus 004 &#xff1a;表示第四个 usb 主控制器&#xff08;机器上总共有四个 usb 主控制器&#xff0c;可以通过命令 lspci | g…

看完这篇文章终于弄明白了什么是 RocketMQ 的存储模型

RocketMQ 优异的性能表现&#xff0c;必然绕不开其优秀的存储模型 。这篇文章&#xff0c;笔者按照自己的理解 , 尝试分析 RocketMQ 的存储模型&#xff0c;希望对大家有所启发。1 整体概览首先温习下 RocketMQ 架构。整体架构中包含四种角色 :Producer &#xff1a;消息发布的…

基于Python深度学习的垃圾分类代码,用深度残差网络构建

垃圾分类 完整代码下载地址&#xff1a;基于Python深度学习的垃圾分类代码 介绍 这是一个基于深度学习的垃圾分类小工程&#xff0c;用深度残差网络构建 软件架构 使用深度残差网络resnet50作为基石&#xff0c;在后续添加需要的层以适应不同的分类任务模型的训练需要用生…

Qt扫盲-QSerialPort理论总结

QSerialPort理论总结一、概述二、使用流程1. 错误处理2. 阻塞串行端口编程3. 非阻塞串行端口编程三、信号四、注意事项一、概述 QSerialPort 类其实就是一个打开串口&#xff0c;进行串口通信传输数据的功能类。我们可以使用QSerialPortInfo帮助类获取有关可用串行端口的信息&…

JavaEE高阶---Spring AOP

一&#xff1a;什么是Spring AOP&#xff1f; 首先&#xff0c;AOP是一种思想&#xff0c;它是对某一类事情的集中处理。 如用户登录权限的效验&#xff0c;没学 AOP 之前&#xff0c;我们所有需要判断用户登录的页面&#xff0c;都要各自实现或调用验证的方法。然后有了 AOP …

【Linux进程间通信】

Linux进程间通信进程间通信介绍进程间通信的概念进程间通信的目的进程间通信的本质进程间通信的分类管道什么是管道匿名管道匿名管道的原理pipe函数匿名管道使用步骤匿名管道读写规则匿名管道的特点匿名管道的四种特殊情况匿名管道的大小命名管道命名管道的原理使用命令创建命名…

【浮点数在内存中的存储规则】

我们知道&#xff0c;整型在内存中的存储比较简单&#xff0c;在内存中都是以二进制来存储的。然而&#xff0c;浮点型在内存中的存储较为复杂。下面来详细探讨&#xff1a; 直接举一个例子&#xff1a; int main() { int n 9; float *pFloat (float *)&n; printf("…

工业树莓派解决传统数据设备数据上云问题

一、前言 工业4.0的浪潮下&#xff0c;许多中小型制造业企业渴望通过数字化转型谋求新的发展动力&#xff0c;然而&#xff0c;在转型之路上常常会面临一个问题&#xff1a;传统数据采集设备数量多、种类杂&#xff0c;不支持比较新颖的现场总线协议或者通信技术&#xff0c;最…

java 微服务框架介绍 SpringCloud Eureka注册中心 Nacos注册中心

为什么要学习微服务框架 认识微服务 服务架构演变 单体架构 分布式架构 微服务结构 SrpingCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址&#xff1a;https://spring.io/projects/spring-cloud。 服务拆分及远程调用 服务拆分注意事项 我们查询的时候需要…