SpringBoot实现返回值数据脱敏

news2025/4/5 2:53:05

介绍

  • SpringBoot实现返回数据脱敏

  • 有时,敏感数据返回时,需要进行隐藏处理,但是如果一个字段一个字段的进行硬编码处理的话,不仅增加了工作量,而且后期需求变动的时候,更加是地狱般的工作量变更。

  • 下面,通过身份证,姓名,密码,手机号等等示例去演示脱敏的流程,当然你也可以在此基础上添加自己的实现方式

原理

  • 项目使用的是SpringBoot,所以需要在序列化的时候,进行脱敏处理,springboot内置的序列化工具为jackson
  • 通过实现com.fasterxml.jackson.databind.JsonSerializer进行自定义序列化
  • 通过重写com.fasterxml.jackson.databind.ser.ContextualSerializer.createContextual获取自定义注解的信息

 实现

自定义注解类

@Target(ElementType.FIELD) //作用于字段上
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside  // 表示自定义自己的注解Sensitive
@JsonSerialize(using = SensitiveInfoSerialize.class) // 该注解使用序列化的方式
public @interface Sensitive {
    SensitizedType value();
}

创建脱敏字段类型枚举

public enum SensitizedType {
    /**
     * 用户id
     */
    USER_ID,
    /**
     * 中文名
     */
    CHINESE_NAME,
    /**
     * 身份证号
     */
    ID_CARD,
    /**
     * 座机号
     */
    FIXED_PHONE,
    /**
     * 手机号
     */
    MOBILE_PHONE,
    /**
     * 地址
     */
    ADDRESS,
    /**
     * 电子邮件
     */
    EMAIL,
    /**
     * 密码
     */
    PASSWORD,
    /**
     * 中国大陆车牌,包含普通车辆、新能源车辆
     */
    CAR_LICENSE,
    /**
     * 银行卡
     */
    BANK_CARD,
    /**
     * IPv4地址
     */
    IPV4,
    /**
     * IPv6地址
     */
    IPV6,
    /**
     * 定义了一个first_mask的规则,只显示第一个字符。
     */
    FIRST_MASK
}

脱敏工具类

import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;

/**
 * @Auther: wu
 * @Date: 2023/7/11
 * @Description: com.wu.demo.common.my_sensitive
 */
public class SensitizedUtil {

    public static String desensitized(CharSequence str, SensitizedType desensitizedType) {
        if (StrUtil.isBlank(str)) {
            return StrUtil.EMPTY;
        }
        String newStr = String.valueOf(str);
        switch (desensitizedType) {
            case USER_ID:
                newStr = String.valueOf(userId());
                break;
            case CHINESE_NAME:
                newStr = chineseName(String.valueOf(str));
                break;
            case ID_CARD:
                newStr = idCardNum(String.valueOf(str), 3, 4);
                break;
            case FIXED_PHONE:
                newStr = fixedPhone(String.valueOf(str));
                break;
            case MOBILE_PHONE:
                newStr = mobilePhone(String.valueOf(str));
                break;
            case ADDRESS:
                newStr = address(String.valueOf(str), 8);
                break;
            case EMAIL:
                newStr = email(String.valueOf(str));
                break;
            case PASSWORD:
                newStr = password(String.valueOf(str));
                break;
            case CAR_LICENSE:
                newStr = carLicense(String.valueOf(str));
                break;
            case BANK_CARD:
                newStr = bankCard(String.valueOf(str));
                break;
            case IPV4:
                newStr = ipv4(String.valueOf(str));
                break;
            case IPV6:
                newStr = ipv6(String.valueOf(str));
                break;
            case FIRST_MASK:
                newStr = firstMask(String.valueOf(str));
                break;
            default:
        }
        return newStr;
    }

    /**
     * 【用户id】不对外提供userId
     *
     * @return 脱敏后的主键
     */
    public static Long userId() {
        return 0L;
    }

    /**
     * 定义了一个first_mask的规则,只显示第一个字符。<br>
     * 脱敏前:123456789;脱敏后:1********。
     *
     * @param str 字符串
     * @return 脱敏后的字符串
     */
    public static String firstMask(String str) {
        if (StrUtil.isBlank(str)) {
            return StrUtil.EMPTY;
        }
        return StrUtil.hide(str, 1, str.length());
    }

    /**
     * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
     *
     * @param fullName 姓名
     * @return 脱敏后的姓名
     */
    public static String chineseName(String fullName) {
        return firstMask(fullName);
    }

    /**
     * 【身份证号】前1位 和后2位
     *
     * @param idCardNum 身份证
     * @param front     保留:前面的front位数;从1开始
     * @param end       保留:后面的end位数;从1开始
     * @return 脱敏后的身份证
     */
    public static String idCardNum(String idCardNum, int front, int end) {
        //身份证不能为空
        if (StrUtil.isBlank(idCardNum)) {
            return StrUtil.EMPTY;
        }
        //需要截取的长度不能大于身份证号长度
        if ((front + end) > idCardNum.length()) {
            return StrUtil.EMPTY;
        }
        //需要截取的不能小于0
        if (front < 0 || end < 0) {
            return StrUtil.EMPTY;
        }
        return StrUtil.hide(idCardNum, front, idCardNum.length() - end);
    }

    /**
     * 【固定电话 前四位,后两位
     *
     * @param num 固定电话
     * @return 脱敏后的固定电话;
     */
    public static String fixedPhone(String num) {
        if (StrUtil.isBlank(num)) {
            return StrUtil.EMPTY;
        }
        return StrUtil.hide(num, 4, num.length() - 2);
    }

    /**
     * 【手机号码】前三位,后4位,其他隐藏,比如135****2210
     *
     * @param num 移动电话;
     * @return 脱敏后的移动电话;
     */
    public static String mobilePhone(String num) {
        if (StrUtil.isBlank(num)) {
            return StrUtil.EMPTY;
        }
        return StrUtil.hide(num, 3, num.length() - 4);
    }

    /**
     * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
     *
     * @param address       家庭住址
     * @param sensitiveSize 敏感信息长度
     * @return 脱敏后的家庭地址
     */
    public static String address(String address, int sensitiveSize) {
        if (StrUtil.isBlank(address)) {
            return StrUtil.EMPTY;
        }
        int length = address.length();
        return StrUtil.hide(address, length - sensitiveSize, length);
    }

    /**
     * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
     *
     * @param email 邮箱
     * @return 脱敏后的邮箱
     */
    public static String email(String email) {
        if (StrUtil.isBlank(email)) {
            return StrUtil.EMPTY;
        }
        int index = StrUtil.indexOf(email, '@');
        if (index <= 1) {
            return email;
        }
        return StrUtil.hide(email, 1, index);
    }

    /**
     * 【密码】密码的全部字符都用*代替,比如:******
     *
     * @param password 密码
     * @return 脱敏后的密码
     */
    public static String password(String password) {
        if (StrUtil.isBlank(password)) {
            return StrUtil.EMPTY;
        }
        return StrUtil.repeat('*', password.length());
    }

    /**
     * 【中国车牌】车牌中间用*代替
     * eg1:null       -》 ""
     * eg1:""         -》 ""
     * eg3:苏D40000   -》 苏D4***0
     * eg4:陕A12345D  -》 陕A1****D
     * eg5:京A123     -》 京A123     如果是错误的车牌,不处理
     *
     * @param carLicense 完整的车牌号
     * @return 脱敏后的车牌
     */
    public static String carLicense(String carLicense) {
        if (StrUtil.isBlank(carLicense)) {
            return StrUtil.EMPTY;
        }
        // 普通车牌
        if (carLicense.length() == 7) {
            carLicense = StrUtil.hide(carLicense, 3, 6);
        } else if (carLicense.length() == 8) {
            // 新能源车牌
            carLicense = StrUtil.hide(carLicense, 3, 7);
        }
        return carLicense;
    }

    /**
     * 银行卡号脱敏
     * eg: 1101 **** **** **** 3256
     *
     * @param bankCardNo 银行卡号
     * @return 脱敏之后的银行卡号
     * @since 5.6.3
     */
    public static String bankCard(String bankCardNo) {
        if (StrUtil.isBlank(bankCardNo)) {
            return bankCardNo;
        }
        bankCardNo = StrUtil.trim(bankCardNo);
        if (bankCardNo.length() < 9) {
            return bankCardNo;
        }

        final int length = bankCardNo.length();
        final int midLength = length - 8;
        final StringBuilder buf = new StringBuilder();

        buf.append(bankCardNo, 0, 4);
        for (int i = 0; i < midLength; ++i) {
            if (i % 4 == 0) {
                buf.append(CharUtil.SPACE);
            }
            buf.append('*');
        }
        buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length);
        return buf.toString();
    }

    /**
     * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。
     *
     * @param ipv4 IPv4地址
     * @return 脱敏后的地址
     */
    public static String ipv4(String ipv4) {
        return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*";
    }

    /**
     * IPv4脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:*
     *
     * @param ipv6 IPv4地址
     * @return 脱敏后的地址
     */
    public static String ipv6(String ipv6) {
        return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*";
    }
}

上述枚举类和脱敏工具类,我使用了hutool中的代码,如果hutool满足你的需求,可以直接把上述自定义注解类和自定义序列化类使用到的SensitizedType类直接替换为hutool中的cn.hutool.core.util.DesensitizedUtil.DesensitizedType的枚举类,

添加自定义序列化实现类

public class SensitiveInfoSerialize extends JsonSerializer<String> implements ContextualSerializer {

    private SensitizedType sensitizedType;

    /**
     * 步骤一
     * 方法来源于ContextualSerializer,获取属性上的注解属性,同时返回一个合适的序列化器
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        // 获取自定义注解
        Sensitive annotation = beanProperty.getAnnotation(Sensitive.class);
        // 注解不为空,且标注的字段为String
        if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){
            this.sensitizedType = annotation.value();
            //自定义情况,返回本序列化器,将顺利进入到该类中的serialize方法中
            return this;
        }
        // 注解为空,字段不为String,寻找合适的序列化器进行处理
        return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
    }

    /**
     * 步骤二
     * 方法来源于JsonSerializer<String>:指定返回类型为String类型,serialize()将修改后的数据返回
     */
    @Override
    public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if(Objects.isNull(sensitizedType)){
            // 定义策略为空,返回原字符串
            jsonGenerator.writeString(str);
        }else {
            // 定义策略不为空,返回策略处理过的字符串
            jsonGenerator.writeString(SensitizedUtil.desensitized(str,sensitizedType));
        }
    }
    
}

测试验证

在需要的脱敏的实体类字段上加上相应的注解

@Data
public class SensitiveBody {
    private String name;
    @Sensitive(SensitizedType.MOBILE_PHONE)
    private String mobile;
    @Sensitive(SensitizedType.ID_CARD)
    private String idCard;
}
    @ApiOperation(value = "脱敏测试处理")
    @GetMapping("sensitiveTest")
    public AjaxResult sensitiveTest(){
        SensitiveBody body = new SensitiveBody();
        body.setMobile("13041064026");
        body.setIdCard("411126189912355689");
        body.setName("Tom");

        return AjaxResult.success(body);
    }

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

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

相关文章

HLS入门简述

What‘s HLS&#xff1f; HLS&#xff08;High-Level Synthesis&#xff09;高层次综合。何谓高层次&#xff1f;意味着我们可以使用高层次的语言来对设计进行描述&#xff0c;如C&#xff0c;C&#xff0c;System C等&#xff1b;那何谓综合呢&#xff1f;HLS在其中起了重要的…

Denosing score matching (公式+代码)

加噪声的分数匹配 def anneal_dsm_score_estimation(scorenet, samples, labels, sigmas, anneal_power2.):# 取出每个样本对应噪声级别下的噪声分布的标准差&#xff0c;即公式中的sigma_i&#xff0c;# 这里的 labels 是用于标识每个样本的噪声级别的&#xff0c;就是 i&…

JavaEE——常见的锁策略、CAS、synchronized 原理(八股)

文章目录 一、常见的锁策略1.乐观锁 & 悲观锁2.轻量级锁 & 重量级锁3.自旋锁 & 挂起等待锁4.互斥锁 & 读写锁5. 公平锁 & 非公平锁 二、CAS1、什么是 CAS2. CAS 的应用场景2.实现自旋锁3. CAS 中的 ABA 问题 三、 Synchronized 原理 一、常见的锁策略 当前…

DataFun: ChatGPT背后的模型详解

ChatGPT背后的模型详解 Overview Transofrmer 各个构件都有一定的作用 Multi-head self attention 每个字的重要性不一样&#xff0c;学习QKV三个矩阵&#xff08;query&#xff0c;key&#xff0c;value&#xff09; 多组QKV RLHF ChatGPT训练过程 思维链 COT

C++_简单模拟实现string的基本结构

C中&#xff0c;string早于STL问世。使用string中的构造函数可以实现对string类型的字符串的一系列操作。 今天来模拟C中的string的基本结构。注意仅仅是简单模拟&#xff0c;string内部结构其实非常复杂&#xff0c;并且不同版本的IDEstring的内部结构也不尽相同。尽管有所不…

SpringBoot2+Vue2实战(十五)高德地图集成

1.地图官网&#xff1a; 高德开放平台 | 高德地图API 2.开发文档(web js) 正式集成&#xff1a; 1.再index.html中引入script标签 <script type"text/javascript" src"https://webapi.amap.com/maps?v2.0&key您申请的key值"></script>…

第五章 PCIe介绍 5.1-5.7

5.1 从PCIe的速度说起 为什么SSD要用PCIe接口&#xff1f;因为它快&#xff0c;比SATA快。 Lane&#xff1a;通道&#xff0c;PCIe最多可以有32个通道。 1. PCIe的工作模式 两个设备之间的PCIe连接&#xff0c;叫做一个Link。如下图&#xff0c;设备A和设备B是个双向连接&#…

【读书笔记】只管去做

《只管去做》是一本很容易读完的书&#xff0c;这本书是以故事的形式来阐述把愿景落实到每天的行动中的方法&#xff0c;对我们做人生规划很有帮助。

使用leaflet在html中加载天地图且去掉左上角的缩放图标以及右下角的logo

前言 我们这一节使用轻量化的javascript库leaflet来实现在html中加载天地图&#xff0c;实现类似高德地图、百度地图的效果。 效果图如下&#xff1a; 话不多说&#xff0c;进入主题&#xff01;&#xff01; 一、注册开发者权限 我们需要在天地图平台注册一个账号&#xff0…

【Qt】VS2013+QT5.6.3环境搭建

安装VS2013 略 安装Qt 安装文件&#xff1a;qt-opensource-windows-x86-msvc2013-5.6.3.exe&#xff08;官网已经不提供下载了。&#xff09; 安装步骤&#xff1a;安装到C盘根目录&#xff0c;其它略。 安装qt vs插件 1、下载地址&#xff1a; https://download.qt.io/a…

string常见功能模拟

学到string终于就不用像c语言一样造轮子了&#xff0c;接下来我们就模拟一下string方便我们更好理解string&#xff0c;首先我们都知道库里有个string&#xff0c;所以为了避免我们的string和库里的冲突&#xff0c;要用命名空间my_string将我们写的string包含在内。string的成…

精准医学时代:探索人工智能在DCA曲线下的临床医学应用

一、引言 在当今医学领域中&#xff0c;精准医学作为一种以个体差异为基础的医疗模式逐渐受到重视和应用[1]。精准医学基于个体基因组、环境和生活方式因素的综合分析&#xff0c;旨在实现个体化的预防、诊断和治疗方案&#xff0c;从而提供更好的临床结果[2]。与传统医学相比&…

MACD进阶版指标公式,提前一天判断MACD金叉

MACD是一种常用的技术分析指标&#xff0c;用于判断价格的趋势和动能&#xff0c;其原理是基于两条指数移动平均线的比较和对价格的平滑处理&#xff0c;MACD金叉是指MACD指标中的快线DIF从下方向上穿过慢线DEA。快线、慢线都是根据收盘价计算出来的&#xff0c;如果想提前一天…

STM32基础知识点总结

一、基础知识点 1、课程体系介绍 单片机概述arm体系结构STM32开发环境搭建 STM32-GPIO编程-点亮世界的那盏灯 STM32-USART串口应用SPI液晶屏 STM32-中断系统 STM32-时钟系统 STM32-ADC DMA 温湿度传感器-DHT11 2.如何学习单片机课程 多听理论、多理解、有问题及时提问 自己多…

论文阅读:基于深度学习的大尺度遥感图像建筑物分割研究

一、该网络中采用了上下文信息捕获模块。通过扩大感受野&#xff0c;在保留细节信息的同时&#xff0c;在中心部分进行多尺度特征的融合&#xff0c;缓解了传统算法中细节信息丢失的问题&#xff1b;通过自适应地融合局部语义特征&#xff0c;该网络在空间特征和通道特征之间建…

时间序列预测 | Matlab基于粒子群算法(PSO)优化径向基神经网络(PSO-RBF)的时间序列预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列预测| Matlab基于粒子群算法(PSO)优化径向基神经网络(PSO-RBF)的时间序列预测 评价指标包括:MAE、MBE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %% 清空环境变量 warni…

2023年开放式蓝牙耳机选购指南!多款热门开放式蓝牙耳机品牌盘点

前言 大家好&#xff0c;作为专注耳机研究多年的发烧级爱好者&#xff0c;毫不夸张地说我为耳机花的钱比买衣服还多&#xff0c;很多人都在问我开放式耳机到底有没有必要买&#xff1f;答案毫无疑问是有必要&#xff01;开放式耳机佩戴舒适又安全的特质让它在耳机届风靡&#…

zabbix server is not running错误解决方法

1.错误&#xff1a;zabbix server is not running 打开zabbix server的时候&#xff0c;底部飘着一行黄色的警告字 2.解决方法 (1)关闭selinux (2)查看日志文件 #tail -f /var/log/zabbix/zabbix_server.log 发现内存溢出了 __zbx_mem_realloc(): out of memory 那…

vitepress使用

vitepress使用 初始化项目 pnpm init pnpm add vitepress vue 创建一个docs文件夹 在docs下新建index.js文件 # Hello VitePress在package.json中增加打包以及运行的指令 "scripts": {"docs:dev": "vitepress dev docs", // 本地运行调试&qu…