SpringBoot项目中新增脱敏功能
admin
2024-01-25 13:52:17
0

SpringBoot项目中新增脱敏功能


项目背景

目前正在开发一个SpringBoot项目,此项目有Web端和微信小程序端。web端提供给工作人员使用,微信小程序提供给群众进行预约操作。项目中有部分敏感数据需要脱敏传递给微信小程序,给与群众查看。


项目需求描述

项目中,由于使用端有两个,对于两个端的数据权限并不一样。Web端可以查看所有数据,小程序端只能查看脱敏后的数据。

需要开发一个通用脱敏功能

  1. 手动进行脱敏操作
  2. 支持多种对象,
  3. 支持不同字段,并脱敏指定字段
  4. 字段的脱敏方式多样
  5. 字段的脱敏方式可自定义

项目解决方案

1. 解决方案

使用注解方式,来支持对指定字段,不同字段,多种脱敏操作,并可以脱离对象。
使用工具对象,通过泛型传参,来支持对不同对象的脱敏操作。

2. 实现代码

2.1 注解 Sensitive

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定义数据脱敏** 例如: 身份证,手机号等信息进行模糊处理** @author lzddddd*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {/*** 脱敏数据类型*/SensitiveType type() default SensitiveType.CUSTOMER;/*** 前置不需要打码的长度*/int prefixNoMaskLen() default 0;/*** 后置不需要打码的长度*/int suffixNoMaskLen() default 0;/*** 用什么打码*/String symbol() default "*";}

2.1 脱敏类型枚举 SensitiveType

public enum SensitiveType {/*** 自定义*/CUSTOMER,/*** 名称**/CHINESE_NAME,/*** 身份证证件号**/ID_CARD_NUM,/*** 手机号**/MOBILE_PHONE,/*** 固定电话*/FIXED_PHONE,/*** 密码**/PASSWORD,/*** 银行卡号*/BANKCARD,/*** 邮箱*/EMAIL,/*** 地址*/ADDRESS,}

2.3 脱敏工具 DesensitizedUtils

import com.ruoyi.common.annotation.Sensitive;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.SensitiveType;
import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Field;
import java.util.*;@Slf4j
public class DesensitizedUtils {/*** 脱敏数据列表*/private List list;/*** 注解列表*/private List fields;/*** 实体对象*/public Class clazz;public DesensitizedUtils(Class clazz){this.clazz = clazz;}/*** 初始化数据** @param list 需要处理数据*/public void init(List list){if (list == null){list = new ArrayList();}this.list = list;// 得到所有定义字段createSensitiveField();}/*** 初始化数据** @param t 需要处理数据*/public void init(T t){list = new ArrayList();if (t != null){list.add(t);}// 得到所有定义字段createSensitiveField();}/*** 得到所有定义字段*/private void createSensitiveField(){this.fields = new ArrayList();List tempFields = new ArrayList<>();tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));for (Field field : tempFields){// 单注解if (field.isAnnotationPresent(Sensitive.class)){putToField(field, field.getAnnotation(Sensitive.class));}// 多注解
//            if (field.isAnnotationPresent(Excels.class))
//            {
//                Excels attrs = field.getAnnotation(Excels.class);
//                Excel[] excels = attrs.value();
//                for (Excel excel : excels)
//                {
//                    putToField(field, excel);
//                }
//            }}}/*** 对list数据源将其里面的数据进行脱敏处理** @param list* @return 结果*/public AjaxResult desensitizedList(List list){if (list == null){return AjaxResult.error("脱敏数据为空");}// 初始化数据this.init(list);int failTimes = 0;for (T t: this.list) {if ((Integer)desensitization(t).get("code") != HttpStatus.SUCCESS){failTimes++;}}if (failTimes >0){return AjaxResult.error("脱敏操作中出现失败",failTimes);}return AjaxResult.success();}/*** 放到字段集合中*/private void putToField(Field field, Sensitive attr){if (attr != null){this.fields.add(new Object[] { field, attr });}}/*** 脱敏:JavaBean模式脱敏** @param t 需要脱敏的对象* @return*/public AjaxResult desensitization(T t) {if (t == null){return AjaxResult.error("脱敏数据为空");}// 初始化数据init(t);try {// 遍历处理需要进行 脱敏的字段for (Object[] os : fields){Field field = (Field) os[0];Sensitive sensitive = (Sensitive) os[1];// 设置实体类私有属性可访问field.setAccessible(true);desensitizeField(sensitive,t,field);}return AjaxResult.success(t);} catch (Exception e) {e.printStackTrace();log.error("日志脱敏处理失败,回滚,详细信息:[{}]", e);return AjaxResult.error("脱敏处理失败",e);}}/*** 对类的属性进行脱敏** @param attr 脱敏参数* @param vo 脱敏对象* @param field 脱敏属性* @return*/private void desensitizeField(Sensitive attr, T vo, Field field) throws IllegalAccessException {if (attr == null || vo == null || field == null){return ;}// 读取对象中的属性Object value = field.get(vo);SensitiveType sensitiveType = attr.type();int prefixNoMaskLen = attr.prefixNoMaskLen();int suffixNoMaskLen = attr.suffixNoMaskLen();String symbol = attr.symbol();//获取属性后现在默认处理的是String类型,其他类型数据可扩展Object val = convertByType(sensitiveType, value, prefixNoMaskLen, suffixNoMaskLen, symbol);field.set(vo, val);}/*** 以类的属性的get方法方法形式获取值** @param o 对象* @param name 属性名* @return value* @throws Exception*/private Object getValue(Object o, String name) throws Exception{if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)){Class clazz = o.getClass();Field field = clazz.getDeclaredField(name);field.setAccessible(true);o = field.get(o);}return o;}/*** 根据不同注解类型处理不同字段*/private Object convertByType(SensitiveType sensitiveType, Object value, int prefixNoMaskLen, int suffixNoMaskLen, String symbol) {switch (sensitiveType) {case CUSTOMER:value = customer(value, prefixNoMaskLen, suffixNoMaskLen, symbol);break;case CHINESE_NAME:value = chineseName(value, symbol);break;case ID_CARD_NUM:value = idCardNum(value, symbol);break;case MOBILE_PHONE:value = mobilePhone(value, symbol);break;case FIXED_PHONE:value = fixedPhone(value, symbol);break;case PASSWORD:value = password(value, symbol);break;case BANKCARD:value = bankCard(value, symbol);break;case EMAIL:value = email(value, symbol);break;case ADDRESS:value = address(value, symbol);break;}return value;}/*--------------------------下面的脱敏工具类也可以单独对某一个字段进行使用-------------------------*//*** 【自定义】 根据设置进行配置** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public Object customer(Object value, int prefixNoMaskLen, int suffixNoMaskLen, String symbol) {//针对字符串的处理if (value instanceof String){return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return value;}/*** 对字符串进行脱敏处理** @param s 需处理数据* @param prefixNoMaskLen 开头展示字符长度* @param suffixNoMaskLen 结尾展示字符长度* @param symbol 填充字符* @return*/private String handleString(String s, int prefixNoMaskLen, int suffixNoMaskLen, String symbol){// 是否为空if (StringUtils.isBlank(s)) {return "";}// 如果设置为空之类使用 * 代替if (StringUtils.isBlank(symbol)){symbol = "*";}// 对长度进行判断int length = s.length();if (length > prefixNoMaskLen + suffixNoMaskLen){String namePrefix = StringUtils.left(s, prefixNoMaskLen);String nameSuffix = StringUtils.right(s, suffixNoMaskLen);s = StringUtils.rightPad(namePrefix, StringUtils.length(s) - suffixNoMaskLen, symbol).concat(nameSuffix);}return s;}/*** 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String chineseName(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示一个字符int prefixNoMaskLen = 1;int suffixNoMaskLen = 0;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【身份证号】显示最后四位,其他隐藏。共计18位或者15位,比如:*************1234** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String idCardNum(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 结尾只展示四个字符int prefixNoMaskLen = 0;int suffixNoMaskLen = 4;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【固定电话】 显示后四位,其他隐藏,比如:*******3241** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String fixedPhone(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 结尾只展示四个字符int prefixNoMaskLen = 0;int suffixNoMaskLen = 4;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【手机号码】前三位,后四位,其他隐藏,比如:135****6810** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String mobilePhone(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示三个字符  结尾只展示四个字符int prefixNoMaskLen = 3;int suffixNoMaskLen = 4;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【地址】只显示到地区,不显示详细地址,比如:湖南省长沙市岳麓区****  只能处理 省市区的数据** @param value 需处理数据* @param symbol 填充字符* @return*/public String address(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示九个字符int prefixNoMaskLen = 9;int suffixNoMaskLen = 0;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【电子邮箱】 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String email(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示一个字符  结尾只展示@及后面的地址int prefixNoMaskLen = 1;int suffixNoMaskLen = 4;String s = (String) value;if (StringUtils.isBlank(s)) {return "";}// 获取最后一个@int lastIndex = StringUtils.lastIndexOf(s, "@");if (lastIndex <= 1) {return s;} else {suffixNoMaskLen = s.length() - lastIndex;}return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【银行卡号】前六位,后四位,其他用星号隐藏每位1个星号,比如:6222600**********1234** @param value 需处理数据* @param symbol 填充字符* @return 脱敏后数据*/public String bankCard(Object value, String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示六个字符  结尾只展示四个字符int prefixNoMaskLen = 6;int suffixNoMaskLen = 4;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}/*** 【密码】密码的全部字符都用*代替,比如:******** @param value 需处理数据* @param symbol 填充字符* @return*/public String password(Object value,String symbol) {//针对字符串的处理if (value instanceof String){// 对前后长度进行设置 默认 开头只展示六个字符  结尾只展示四个字符int prefixNoMaskLen = 0;int suffixNoMaskLen = 0;return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);}return "";}
}

3 使用实例

3.1 需注解对象

public class User {private static final long serialVersionUID = 1L;/** 普通用户ID */private Long userId;/** 昵称 */@Excel(name = "昵称")@Sensitive(type = SensitiveType.CUSTOMER,prefixNoMaskLen = 2,suffixNoMaskLen = 1)private String nickName;/** 姓名 */@Excel(name = "姓名")@Sensitive(type = SensitiveType.CHINESE_NAME)private String userName;/** 身份证 */@Excel(name = "身份证")@Sensitive(type = SensitiveType.ID_CARD_NUM)private String identityCard;/** 手机号码 */@Excel(name = "手机号码")@Sensitive(type = SensitiveType.MOBILE_PHONE)private String phoneNumber;
}

3.2 脱敏操作

// 脱敏对象User user = new User();......DesensitizedUtils desensitizedUtils = new DesensitizedUtils<>(User.class);desensitizedUtils.desensitization(user);//脱敏队列List users = new ArrayList<>();......DesensitizedUtils desensitizedUtils = new DesensitizedUtils<>(User.class);desensitizedUtils.desensitizedList(users);

相关内容

热门资讯

男孩爱哭是什么原因 男孩爱哭是什么原因孩子哭是天生的,在他们一出生的时候就会哭,所以哭是他们唯一不用学习的事情,也是他们...
小学生开学前疯狂赶作业:妈妈气... 小学生开学前疯狂赶作业:妈妈气出心脏病说起孩子写作业是很多家长比较头疼的一件事,老师布置作业之后,要...
暗黑3两分钟就能干掉成就怎么完... 暗黑3两分钟就能干掉成就怎么完成 猎魔人速想听实话么。猎魔人装备如果不行,2分钟是打不了的。大多数职...
异形是什么意思 异形是什么意思  异形释义:  1.不同于一般类型,表现多种不同类型 2.发育不同阶段有不同形状的 ...
北京到广州飞机几个小时? 北京到广州飞机几个小时?北京到广州的飞机飞行时间大约为**3个半小时**。具体的肆芦瞎飞行时间可能哗...
失恋失去的到底是什么? 失恋失去的到底是什么?失去的是一个伤害自己的人,一段糟糕的感情,而获得的却是重生,所以失恋没必要去难...
铁碎牙,天生牙,乾坤刀,斗鬼神... 铁碎牙,天生牙,乾坤刀,斗鬼神,这四把刀哪个最厉害?犬夜叉里面的铁碎牙第一,乾坤刀第二,斗鬼神第三,...
男人对女人说!懂你为情感,懂你... 男人对女人说!懂你为情感,懂你为守候是什么意思?男人对女人说,懂你为情感,懂你为守候的意思是:男人懂...
风起霓裳中李治的结局如何? 风起霓裳中李治的结局如何?在里面的结局还是非常不错的,而且这也是一部超级好看的作品。最后的大结局也非...
不要停下来,请继续往前走。的英... 不要停下来,请继续往前走。的英文是什么?Don't stop. ______ ______ go不要...
暗恋一个人会有结果吗? 暗恋一个人会有结果吗?我觉得如果暗恋一个人,但是一直不跟他说的话,是不会有结果的。只在心底默默的喜欢...
赛尔号异能星怎么去 赛尔号异能星怎么去真笨,这都进不去在裂空星系 ,或者你直接去任务里面可以直接传送过去打开地图,裂空星...
暗能量为什么能让宇宙膨胀,它真... 暗能量为什么能让宇宙膨胀,它真的可以撕碎宇宙?它会撕碎宇宙的,这样的能量让宇宙一直在变化。它有节奏的...
露营睡帐篷不怕坏人吗 露营睡帐篷不怕坏人吗 选择安全的坏人多的地方,坏人也不是到处都是。注意些,同时带上强力伙伴
一家之主有什么含义 一家之主有什么含义  一家之主释义:  家庭的当家人  [拼音] [yī jiā zhī zh...
鱼龙有哪些特点? 鱼龙有哪些特点?鱼龙早在三叠纪中期就出现了,它是恐龙的同代动物。在中生代,或庆它们都是海洋里的“霸主...
古代怎么说四月十九日 古代怎么说四月十九日  4月19日是公历一年中的第109天(闰年第110天),离全年的结束还有256...
一些好听的伤感歌曲。旋律好的。... 一些好听的伤感歌曲。旋律好的。会让一个人单曲循环的歌如题。越多越好。不要说唱的Take a bow这...
女儿国传奇漫画全集免费 女儿国传奇漫画全集免费画画不容易,要尊重作者
闷鸡子啄白米的人性格 闷鸡子啄白米的人性格保守、安稳、不喜变化。闷鸡子啄白米是中国民间流传的故事,用来形容那些追求安逸、不...