SpringBoot项目中新增脱敏功能
项目背景
目前正在开发一个SpringBoot项目,此项目有Web端和微信小程序端。web端提供给工作人员使用,微信小程序提供给群众进行预约操作。项目中有部分敏感数据需要脱敏传递给微信小程序,给与群众查看。
项目需求描述
项目中,由于使用端有两个,对于两个端的数据权限并不一样。Web端可以查看所有数据,小程序端只能查看脱敏后的数据。
需要开发一个通用脱敏功能
手动进行脱敏操作 支持多种对象, 支持不同字段,并脱敏指定字段 字段的脱敏方式多样 字段的脱敏方式可自定义
项目解决方案
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);