前言
在开发系统过程中,经常会接触到大量的数据信息,这些数据信息可能包含身份证号、手机号、姓名、卡号等各种敏感信息。而有些用户需求是不允许,这些敏感信息数据为了保护用户个人信息的安全。对这些数据需要进行特殊处理。
一、什么是数据脱敏?
数据脱敏又称数据去隐私化或数据变形,是在给定的规则、策略下对敏感数据进行变换、修改的技术机制,能够在很大程度上解决敏感数据在非可信环境中使用的问题。根据数据保护规范和脱敏策略.对业务数据中的敏感信息实施自动变形.实现对敏感信息的隐藏。
二、实现
适合单个字段
1、枚举需要脱敏的字段类型
/**
* 需要脱敏的字段类型
*
* @author xh
* @Date 2022/7/20
*/
public enum SensitiveTypeEnum {
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 座机号
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 银行卡
*/
BANK_CARD,
/**
* 虚拟账号
*/
ACCOUNT,
/**
* 密码
*/
PASSWORD;
}
2、创建注解,标识需要脱敏的字段
import com.artboy.project.common.enums.SensitiveTypeEnum;
import java.lang.annotation.*;
/**
* @author xh
* @Date 2022/7/20
*/
// 指定在注解使用的位置
@Target({ElementType.FIELD, ElementType.METHOD})
// 指定注解保存的范围
@Retention(RetentionPolicy.RUNTIME)
// 可以被子类继承
@Inherited
// 设置为文档说明
@Documented
public @interface Desensitized {
/*脱敏类型(规则)*/
SensitiveTypeEnum type();
/*判断注解是否生效的方法*/
String isEffictiveMethod() default "";
}
创建Object工具类
用于复制对象和对对象的其他操作,注意使用fastjson实现深拷贝对于复杂的对象会出现栈溢出,这里修改为了Gson
package com.artboy.project.common.utils;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* @author xh
* @Date 2022/7/26
*/
public class DesensitizedObjectUtils {
/**
* 用序列化-反序列化方式实现深克隆
* 缺点:1、被拷贝的对象必须要实现序列化
*
* @param obj
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T deepCloneObject(T obj) {
T t = (T) new Object();
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(obj);
out.close();
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
t = (T) in.readObject();
in.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return t;
}
/**
* 用序列化-反序列化的方式实现深克隆
* 这里使用GSON来取代FAST-JSON
* 缺点:1、当实体中存在接口类型的参数,并且这个接口指向的实例为枚举类型时,会报错"com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 171, fieldName iLimitKey"
*
* @param objSource
* @return
*/
public static Object deepCloneByFastJson(Object objSource) {
// String tempJson = JSON.toJSONString(objSource);
// Object clone = JSON.parseObject(tempJson, objSource.getClass());
Gson gson = new Gson();
String tempJson = gson.toJson(objSource);
Object clone = gson.fromJson(tempJson, objSource.getClass());
return clone;
}
/**
* 深度克隆对象
*
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static Object deepClone(Object objSource) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
if (null == objSource) return null;
//是否jdk类型、基础类型、枚举类型
if (isJDKType(objSource.getClass())
|| objSource.getClass().isPrimitive()
|| objSource instanceof Enum<?>) {
if ("java.lang.String".equals(objSource.getClass().getName())) {//目前只支持String类型深复制
return new String((String) objSource);
} else {
return objSource;
}
}
// 获取源对象类型
Class<?> clazz = objSource.getClass();
Object objDes = clazz.newInstance();
// 获得源对象所有属性
Field[] fields = getAllFields(objSource);
// 循环遍历字段,获取字段对应的属性值
for (Field field : fields) {
field.setAccessible(true);
if (null == field) continue;
Object value = field.get(objSource);
if (null == value) continue;
Class<?> type = value.getClass();
if (isStaticFinal(field)) {
continue;
}
try {
//遍历集合属性
if (type.isArray()) {//对数组类型的字段进行递归过滤
int len = Array.getLength(value);
if (len < 1) continue;
Class<?> c = value.getClass().getComponentType();
Array newArray = (Array) Array.newInstance(c, len);
for (int i = 0; i < len; i++) {
Object arrayObject = Array.get(value, i);
Array.set(newArray, i, deepClone(arrayObject));
}
} else if (value instanceof Collection<?>) {
Collection newCollection = (Collection) value.getClass().newInstance();
Collection<?> c = (Collection<?>) value;
Iterator<?> it = c.iterator();
while (it.hasNext()) {
Object collectionObj = it.next();
newCollection.add(deepClone(collectionObj));
}
field.set(objDes, newCollection);
continue;
} else if (value instanceof Map<?, ?>) {
Map newMap = (Map) value.getClass().newInstance();
Map<?, ?> m = (Map<?, ?>) value;
Set<?> set = m.entrySet();
for (Object o : set) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object mapVal = entry.getValue();
newMap.put(entry.getKey(), deepClone(mapVal));
}
field.set(objDes, newMap);
continue;
}
//是否jdk类型或基础类型
if (isJDKType(field.get(objSource).getClass())
|| field.getClass().isPrimitive()
|| isStaticType(field)
|| value instanceof Enum<?>) {
if ("java.lang.String".equals(value.getClass().getName())) {//目前只支持String类型深复制
field.set(objDes, new String((String) value));
} else {
field.set(objDes, field.get(objSource));
}
continue;
}
//是否枚举
if (value.getClass().isEnum()) {
field.set(objDes, field.get(objSource));
continue;
}
//是否自定义类
if (isUserDefinedType(value.getClass())) {
field.set(objDes, deepClone(value));
continue;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return objDes;
}
/**
* 是否静态变量
*
* @param field
* @return
*/
private static boolean isStaticType(Field field) {
return field.getModifiers() == 8 ? true : false;
}
private static boolean isStaticFinal(Field field) {
return Modifier.isFinal(field.getModifiers()) && Modifier.isStatic(field.getModifiers());
}
/**
* 是否jdk类型变量
*
* @param clazz
* @return
* @throws IllegalAccessException
*/
private static boolean isJDKType(Class clazz) throws IllegalAccessException {
//Class clazz = field.get(objSource).getClass();
return org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "javax.")
|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "java.")
|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "javax.")
|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "java.");
}
/**
* 是否用户自定义类型
*
* @param clazz
* @return
*/
private static boolean isUserDefinedType(Class<?> clazz) {
return
clazz.getPackage() != null
&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "javax.")
&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "java.")
&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "javax.")
&& !StringUtils.startsWith(clazz.getName(), "java.");
}
/**
* 获取包括父类所有的属性
*
* @param objSource
* @return
*/
public static Field[] getAllFields(Object objSource) {
/*获得当前类的所有属性(private、protected、public)*/
List<Field> fieldList = new ArrayList<Field>();
Class tempClass = objSource.getClass();
while (tempClass != null && !tempClass.getName().toLowerCase().equals("java.lang.object")) {//当父类为null的时候说明到达了最上层的父类(Object类).
fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
tempClass = tempClass.getSuperclass(); //得到父类,然后赋给自己
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
/**
* 深度克隆对象
*
* @throws IllegalAccessException
* @throws InstantiationException
*/
@Deprecated
public static Object copy(Object objSource) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
if (null == objSource) return null;
// 获取源对象类型
Class<?> clazz = objSource.getClass();
Object objDes = clazz.newInstance();
// 获得源对象所有属性
Field[] fields = getAllFields(objSource);
// 循环遍历字段,获取字段对应的属性值
for (Field field : fields) {
field.setAccessible(true);
// 如果该字段是 static + final 修饰
if (field.getModifiers() >= 24) {
continue;
}
try {
// 设置字段可见,即可用get方法获取属性值。
field.set(objDes, field.get(objSource));
} catch (Exception e) {
e.printStackTrace();
}
}
return objDes;
}
}
创建脱敏工具类
package com.artboy.project.common.utils;
import com.artboy.project.common.annotation.Desensitized;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author xh
* @Date 2022/7/26
*/
public class DesensitizedUtils {
/**
* 获取脱敏json串(递归引用会导致java.lang.StackOverflowError)
*
* @param javaBean
* @return
*/
public static String getJson(Object javaBean) {
String json = null;
if (null != javaBean) {
try {
if (javaBean.getClass().isInterface()) return json;
/* 克隆出一个实体进行字段修改,避免修改原实体 */
//Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);
//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);
Object clone = DesensitizedObjectUtils.deepClone(javaBean);
/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */
Set<Integer> referenceCounter = new HashSet<Integer>();
/* 对克隆实体进行脱敏操作 */
DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);
/* 利用fastjson对脱敏后的克隆对象进行序列化 */
// json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);
json = new Gson().toJson(clone);
/* 清空计数器 */
referenceCounter.clear();
referenceCounter = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
return json;
}
public static <T> T getObj(T javaBean) {
T clone = null;
if (null != javaBean) {
try {
if (javaBean.getClass().isInterface()) return null;
/* 克隆出一个实体进行字段修改,避免修改原实体 */
//Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);
//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);
clone = (T) DesensitizedObjectUtils.deepClone(javaBean);
/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */
Set<Integer> referenceCounter = new HashSet<Integer>();
/* 对克隆实体进行脱敏操作 */
DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);
/* 清空计数器 */
referenceCounter.clear();
referenceCounter = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
return clone;
}
/**
* 对需要脱敏的字段进行转化
*
* @param fields
* @param javaBean
* @param referenceCounter
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException {
if (null != fields && fields.length > 0) {
for (Field field : fields) {
field.setAccessible(true);
if (null != field && null != javaBean) {
Object value = field.get(javaBean);
if (null != value) {
Class<?> type = value.getClass();
//处理子属性,包括集合中的
if (type.isArray()) {//对数组类型的字段进行递归过滤
int len = Array.getLength(value);
for (int i = 0; i < len; i++) {
Object arrayObject = Array.get(value, i);
if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter);
}
}
} else if (value instanceof Collection<?>) {//对集合类型的字段进行递归过滤
Collection<?> c = (Collection<?>) value;
Iterator<?> it = c.iterator();
while (it.hasNext()) {// TODO: 待优化
Object collectionObj = it.next();
if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter);
}
}
} else if (value instanceof Map<?, ?>) {//对Map类型的字段进行递归过滤
Map<?, ?> m = (Map<?, ?>) value;
Set<?> set = m.entrySet();
for (Object o : set) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object mapVal = entry.getValue();
if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(mapVal), mapVal, referenceCounter);
}
}
} else if (value instanceof Enum<?>) {
continue;
}
/*除基础类型、jdk类型的字段之外,对其他类型的字段进行递归过滤*/
else {
if (!type.isPrimitive()
&& type.getPackage() != null
&& !StringUtils.startsWith(type.getPackage().getName(), "javax.")
&& !StringUtils.startsWith(type.getPackage().getName(), "java.")
&& !StringUtils.startsWith(field.getType().getName(), "javax.")
&& !StringUtils.startsWith(field.getName(), "java.")
&& referenceCounter.add(value.hashCode())) {
replace(DesensitizedObjectUtils.getAllFields(value), value, referenceCounter);
}
}
}
//脱敏操作
setNewValueForField(javaBean, field, value);
}
}
}
}
/**
* 排除基础类型、jdk类型、枚举类型的字段
*
* @param clazz
* @param value
* @param referenceCounter
* @return
*/
private static boolean isNotGeneralType(Class<?> clazz, Object value, Set<Integer> referenceCounter) {
return !clazz.isPrimitive()
&& clazz.getPackage() != null
&& !clazz.isEnum()
&& !StringUtils.startsWith(clazz.getPackage().getName(), "javax.")
&& !StringUtils.startsWith(clazz.getPackage().getName(), "java.")
&& !StringUtils.startsWith(clazz.getName(), "javax.")
&& !StringUtils.startsWith(clazz.getName(), "java.")
&& referenceCounter.add(value.hashCode());
}
/**
* 脱敏操作(按照规则转化需要脱敏的字段并设置新值)
* 目前只支持String类型的字段,如需要其他类型如BigDecimal、Date等类型,可以添加
*
* @param javaBean
* @param field
* @param value
* @throws IllegalAccessException
*/
public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException {
//处理自身的属性
Desensitized annotation = field.getAnnotation(Desensitized.class);
if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) {
String valueStr = (String) value;
if (StringUtils.isNotBlank(valueStr)) {
switch (annotation.type()) {
case CHINESE_NAME: {
field.set(javaBean, DesensitizedUtils.chineseName(valueStr));
break;
}
case ID_CARD: {
field.set(javaBean, DesensitizedUtils.idCardNum(valueStr));
break;
}
case FIXED_PHONE: {
field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr));
break;
}
case MOBILE_PHONE: {
field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr));
break;
}
case ADDRESS: {
field.set(javaBean, DesensitizedUtils.address(valueStr, 7));
break;
}
case EMAIL: {
field.set(javaBean, DesensitizedUtils.email(valueStr));
break;
}
case BANK_CARD: {
field.set(javaBean, DesensitizedUtils.bankCard(valueStr));
break;
}
case PASSWORD: {
field.set(javaBean, DesensitizedUtils.password(valueStr));
break;
}case ACCOUNT:{
field.set(javaBean, DesensitizedUtils.account(valueStr));
break;
}
}
}
}
}
/**
* 执行某个对象中指定的方法
*
* @param javaBean 对象
* @param desensitized
* @return
*/
private static boolean executeIsEffictiveMethod(Object javaBean, Desensitized desensitized) {
boolean isAnnotationEffictive = true;//注解默认生效
if (desensitized != null) {
String isEffictiveMethod = desensitized.isEffictiveMethod();
if (isNotEmpty(isEffictiveMethod)) {
try {
Method method = javaBean.getClass().getMethod(isEffictiveMethod);
method.setAccessible(true);
isAnnotationEffictive = (Boolean) method.invoke(javaBean);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
return isAnnotationEffictive;
}
private static boolean isNotEmpty(String str) {
return str != null && !"".equals(str);
}
private static boolean isEmpty(String str) {
return !isNotEmpty(str);
}
/**
* 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
*
* @param fullName
* @return
*/
public static String chineseName(String fullName) {
if (StringUtils.isBlank(fullName)) {
return "";
}
String name = StringUtils.left(fullName, 1);
return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
}
/**
* 【身份证号】显示第一位和最后一位
*
* @param id
* @return
*/
public static String idCardNum(String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));
}
/**
* 【虚拟账号】显示第一位和最后一位
*
* @param id
* @return
*/
public static String account(String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));
}
/**
* 【固定电话 后四位,其他隐藏,比如1234
*
* @param num
* @return
*/
public static String fixedPhone(String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
}
/**
* 【手机号码】前三位,后四位,其他隐藏,比如135****6810
*
* @param num
* @return
*/
public static String mobilePhone(String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));
}
/**
* 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
*
* @param address
* @param sensitiveSize 敏感信息长度
* @return
*/
public static String address(String address, int sensitiveSize) {
if (StringUtils.isBlank(address)) {
return "";
}
int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}
/**
* 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com>
*
* @param email
* @return
*/
public static String email(String email) {
if (StringUtils.isBlank(email)) {
return "";
}
int index = StringUtils.indexOf(email, "@");
if (index <= 1)
return email;
else
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
/**
* 【银行卡号】前4位,后3位,其他用星号隐藏每位1个星号,比如:6217 **** **** **** 567>
*
* @param cardNum
* @return
*/
public static String bankCard(String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 3), StringUtils.length(cardNum), "*"), "****"));
}
/**
* 【密码】密码的全部字符都用*代替,比如:******
*
* @param password
* @return
*/
public static String password(String password) {
if (StringUtils.isBlank(password)) {
return "";
}
String pwd = StringUtils.left(password, 0);
return StringUtils.rightPad(pwd, StringUtils.length(password), "*");
}
/**
* 遍历List脱敏数据
* @param content
* @return
*/
public static <T> List getList(List<T> content){
if (content == null || content.size() <= 0) {
return content;
}
List list = new ArrayList<T>();
for (T t : content) {
list.add(getObj(t));
}
return list;
}
}
创建脱敏工具类
package com.artboy.project.common.utils;
import com.artboy.project.common.annotation.Desensitized;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author xh
* @Date 2022/7/26
*/
public class DesensitizedUtils {
/**
* 获取脱敏json串(递归引用会导致java.lang.StackOverflowError)
*
* @param javaBean
* @return
*/
public static String getJson(Object javaBean) {
String json = null;
if (null != javaBean) {
try {
if (javaBean.getClass().isInterface()) return json;
/* 克隆出一个实体进行字段修改,避免修改原实体 */
//Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);
//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);
Object clone = DesensitizedObjectUtils.deepClone(javaBean);
/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */
Set<Integer> referenceCounter = new HashSet<Integer>();
/* 对克隆实体进行脱敏操作 */
DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);
/* 利用fastjson对脱敏后的克隆对象进行序列化 */
// json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);
json = new Gson().toJson(clone);
/* 清空计数器 */
referenceCounter.clear();
referenceCounter = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
return json;
}
public static <T> T getObj(T javaBean) {
T clone = null;
if (null != javaBean) {
try {
if (javaBean.getClass().isInterface()) return null;
/* 克隆出一个实体进行字段修改,避免修改原实体 */
//Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);
//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);
clone = (T) DesensitizedObjectUtils.deepClone(javaBean);
/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */
Set<Integer> referenceCounter = new HashSet<Integer>();
/* 对克隆实体进行脱敏操作 */
DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);
/* 清空计数器 */
referenceCounter.clear();
referenceCounter = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
return clone;
}
/**
* 对需要脱敏的字段进行转化
*
* @param fields
* @param javaBean
* @param referenceCounter
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException {
if (null != fields && fields.length > 0) {
for (Field field : fields) {
field.setAccessible(true);
if (null != field && null != javaBean) {
Object value = field.get(javaBean);
if (null != value) {
Class<?> type = value.getClass();
//处理子属性,包括集合中的
if (type.isArray()) {//对数组类型的字段进行递归过滤
int len = Array.getLength(value);
for (int i = 0; i < len; i++) {
Object arrayObject = Array.get(value, i);
if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter);
}
}
} else if (value instanceof Collection<?>) {//对集合类型的字段进行递归过滤
Collection<?> c = (Collection<?>) value;
Iterator<?> it = c.iterator();
while (it.hasNext()) {// TODO: 待优化
Object collectionObj = it.next();
if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter);
}
}
} else if (value instanceof Map<?, ?>) {//对Map类型的字段进行递归过滤
Map<?, ?> m = (Map<?, ?>) value;
Set<?> set = m.entrySet();
for (Object o : set) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object mapVal = entry.getValue();
if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {
replace(DesensitizedObjectUtils.getAllFields(mapVal), mapVal, referenceCounter);
}
}
} else if (value instanceof Enum<?>) {
continue;
}
/*除基础类型、jdk类型的字段之外,对其他类型的字段进行递归过滤*/
else {
if (!type.isPrimitive()
&& type.getPackage() != null
&& !StringUtils.startsWith(type.getPackage().getName(), "javax.")
&& !StringUtils.startsWith(type.getPackage().getName(), "java.")
&& !StringUtils.startsWith(field.getType().getName(), "javax.")
&& !StringUtils.startsWith(field.getName(), "java.")
&& referenceCounter.add(value.hashCode())) {
replace(DesensitizedObjectUtils.getAllFields(value), value, referenceCounter);
}
}
}
//脱敏操作
setNewValueForField(javaBean, field, value);
}
}
}
}
/**
* 排除基础类型、jdk类型、枚举类型的字段
*
* @param clazz
* @param value
* @param referenceCounter
* @return
*/
private static boolean isNotGeneralType(Class<?> clazz, Object value, Set<Integer> referenceCounter) {
return !clazz.isPrimitive()
&& clazz.getPackage() != null
&& !clazz.isEnum()
&& !StringUtils.startsWith(clazz.getPackage().getName(), "javax.")
&& !StringUtils.startsWith(clazz.getPackage().getName(), "java.")
&& !StringUtils.startsWith(clazz.getName(), "javax.")
&& !StringUtils.startsWith(clazz.getName(), "java.")
&& referenceCounter.add(value.hashCode());
}
/**
* 脱敏操作(按照规则转化需要脱敏的字段并设置新值)
* 目前只支持String类型的字段,如需要其他类型如BigDecimal、Date等类型,可以添加
*
* @param javaBean
* @param field
* @param value
* @throws IllegalAccessException
*/
public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException {
//处理自身的属性
Desensitized annotation = field.getAnnotation(Desensitized.class);
if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) {
String valueStr = (String) value;
if (StringUtils.isNotBlank(valueStr)) {
switch (annotation.type()) {
case CHINESE_NAME: {
field.set(javaBean, DesensitizedUtils.chineseName(valueStr));
break;
}
case ID_CARD: {
field.set(javaBean, DesensitizedUtils.idCardNum(valueStr));
break;
}
case FIXED_PHONE: {
field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr));
break;
}
case MOBILE_PHONE: {
field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr));
break;
}
case ADDRESS: {
field.set(javaBean, DesensitizedUtils.address(valueStr, 7));
break;
}
case EMAIL: {
field.set(javaBean, DesensitizedUtils.email(valueStr));
break;
}
case BANK_CARD: {
field.set(javaBean, DesensitizedUtils.bankCard(valueStr));
break;
}
case PASSWORD: {
field.set(javaBean, DesensitizedUtils.password(valueStr));
break;
}case ACCOUNT:{
field.set(javaBean, DesensitizedUtils.account(valueStr));
break;
}
}
}
}
}
/**
* 执行某个对象中指定的方法
*
* @param javaBean 对象
* @param desensitized
* @return
*/
private static boolean executeIsEffictiveMethod(Object javaBean, Desensitized desensitized) {
boolean isAnnotationEffictive = true;//注解默认生效
if (desensitized != null) {
String isEffictiveMethod = desensitized.isEffictiveMethod();
if (isNotEmpty(isEffictiveMethod)) {
try {
Method method = javaBean.getClass().getMethod(isEffictiveMethod);
method.setAccessible(true);
isAnnotationEffictive = (Boolean) method.invoke(javaBean);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
return isAnnotationEffictive;
}
private static boolean isNotEmpty(String str) {
return str != null && !"".equals(str);
}
private static boolean isEmpty(String str) {
return !isNotEmpty(str);
}
/**
* 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
*
* @param fullName
* @return
*/
public static String chineseName(String fullName) {
if (StringUtils.isBlank(fullName)) {
return "";
}
String name = StringUtils.left(fullName, 1);
return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
}
/**
* 【身份证号】显示第一位和最后一位
*
* @param id
* @return
*/
public static String idCardNum(String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));
}
/**
* 【虚拟账号】显示第一位和最后一位
*
* @param id
* @return
*/
public static String account(String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));
}
/**
* 【固定电话 后四位,其他隐藏,比如1234
*
* @param num
* @return
*/
public static String fixedPhone(String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
}
/**
* 【手机号码】前三位,后四位,其他隐藏,比如135****6810
*
* @param num
* @return
*/
public static String mobilePhone(String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));
}
/**
* 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
*
* @param address
* @param sensitiveSize 敏感信息长度
* @return
*/
public static String address(String address, int sensitiveSize) {
if (StringUtils.isBlank(address)) {
return "";
}
int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}
/**
* 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com>
*
* @param email
* @return
*/
public static String email(String email) {
if (StringUtils.isBlank(email)) {
return "";
}
int index = StringUtils.indexOf(email, "@");
if (index <= 1)
return email;
else
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
/**
* 【银行卡号】前4位,后3位,其他用星号隐藏每位1个星号,比如:6217 **** **** **** 567>
*
* @param cardNum
* @return
*/
public static String bankCard(String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 3), StringUtils.length(cardNum), "*"), "****"));
}
/**
* 【密码】密码的全部字符都用*代替,比如:******
*
* @param password
* @return
*/
public static String password(String password) {
if (StringUtils.isBlank(password)) {
return "";
}
String pwd = StringUtils.left(password, 0);
return StringUtils.rightPad(pwd, StringUtils.length(password), "*");
}
/**
* 遍历List脱敏数据
* @param content
* @return
*/
public static <T> List getList(List<T> content){
if (content == null || content.size() <= 0) {
return content;
}
List list = new ArrayList<T>();
for (T t : content) {
list.add(getObj(t));
}
return list;
}
}
测试
这里我们以UserEntity为例:(一定要序列化)
package com.artboy.project.model.entity;
import com.artboy.project.common.annotation.Desensitized;
import com.artboy.project.common.enums.SensitiveTypeEnum;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
* 用户
*
* @TableName user
*/
@TableName(value = "user")
@Data
public class UserEntity implements Serializable {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 用户昵称
*/
private String username;
/**
* 账号
*/
private String userAccount;
/**
* 用户头像
*/
private String avatarUrl;
/**
* 性别
*/
private Integer gender;
/**
* 密码
*/
@Desensitized(type = SensitiveTypeEnum.PASSWORD)
private String userPassword;
/**
* 电话
*/
@Desensitized(type = SensitiveTypeEnum.MOBILE_PHONE)
private String phone;
/**
* 邮箱
*/
@Desensitized(type = SensitiveTypeEnum.EMAIL)
private String email;
/**
* 状态 0 - 正常
*/
private Integer userStatus;
/**
* 创建时间,数据库已设置自动更新
*/
private Date createTime;
/**
* 更新时间,数据库已设置自动更新
*/
private Date updateTime;
/**
* 是否删除
*/
@TableLogic
private Integer isDelete;
/**
* 用户角色 0 - 普通用户 1 - 管理员
*/
private Integer userRole;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
测试方法:
QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
UserEntity user = userMapper.selectOne(queryWrapper);
UserEntity safetyUser = DesensitizedUtils.getObj(user);
Springboot切面+注解实现数据脱敏
处理步骤
新建脱敏的枚举类
package com.fwy.common.config.dataRule;
import lombok.Getter;
/**
* @description:隐私数据类型枚举
* @author: xiaYZ
* @createDate: 2022/6/21
*/
@Getter
public enum PrivacyTypeEnum {
/** 自定义(此项需设置脱敏的范围)*/
CUSTOMER,
/** 姓名 */
NAME,
/** 身份证号 */
ID_CARD,
/** 手机号 */
PHONE,
/** 邮箱 */
EMAIL,
}
新建脱敏操作的工具类
package com.fwy.common.config.dataRule;
/**
* @description:
* @author: xiaYZ
* @createDate: 2022/6/21
*/
public class PrivacyUtil {
/**
* 隐藏手机号中间四位
*/
public static String hidePhone(String phone) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/**
* 隐藏邮箱
*/
public static String hideEmail(String email) {
return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
}
/**
* 隐藏身份证
*/
public static String hideIDCard(String idCard) {
return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
}
/**
* 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任**
*/
public static String hideChineseName(String chineseName) {
if (chineseName == null) {
return null;
}
return desValue(chineseName, 1, 0, "*");
}
// /**
// * 【身份证号】显示前4位, 后2位
// */
// public static String hideIdCard(String idCard) {
// return desValue(idCard, 4, 2, "*");
// }
// /**
// * 【手机号码】前三位,后四位,其他隐藏。
// */
// public static String hidePhone(String phone) {
// return desValue(phone, 3, 4, "*");
// }
/**
* 对字符串进行脱敏操作
* @param origin 原始字符串
* @param prefixNoMaskLen 左侧需要保留几位明文字段
* @param suffixNoMaskLen 右侧需要保留几位明文字段
* @param maskStr 用于遮罩的字符串, 如'*'
* @return 脱敏后结果
*/
public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
if (origin == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0, n = origin.length(); i < n; i++) {
if (i < prefixNoMaskLen) {
sb.append(origin.charAt(i));
continue;
}
if (i > (n - suffixNoMaskLen - 1)) {
sb.append(origin.charAt(i));
continue;
}
sb.append(maskStr);
}
return sb.toString();
}
}
申明注解类
package com.fwy.common.config.dataRule;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @description: 自定义数据脱敏注解
* @author: xiaYZ
* @createDate: 2022/6/21
*/
@Target(ElementType.FIELD) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // class文件中保留,运行时也保留,能通过反射读取到
@JacksonAnnotationsInside // 表示自定义自己的注解PrivacyEncrypt
@JsonSerialize(using = PrivacySerializer.class) // 该注解使用序列化的方式
public @interface PrivacyEncrypt {
/**
* 脱敏数据类型(没给默认值,所以使用时必须指定type)
*/
PrivacyTypeEnum type();
/**
* 前置不需要打码的长度
*/
int prefixNoMaskLen() default 1;
/**
* 后置不需要打码的长度
*/
int suffixNoMaskLen() default 1;
/**
* 用什么打码
*/
String symbol() default "*";
}
注解的AOP操作
package com.fwy.common.config.dataRule;
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 lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.Objects;
/**
* @description:
* @author: xiaYZ
* @createDate: 2022/6/21
*/
@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {
// 脱敏类型
private PrivacyTypeEnum privacyTypeEnum;
// 前几位不脱敏
private Integer prefixNoMaskLen;
// 最后几位不脱敏
private Integer suffixNoMaskLen;
// 用什么打码
private String symbol;
@Override
public void serialize(final String origin, final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider) throws IOException {
if (StringUtils.isNotBlank(origin) && null != privacyTypeEnum) {
switch (privacyTypeEnum) {
case CUSTOMER:
jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));
break;
case NAME:
jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));
break;
case ID_CARD:
jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));
break;
case PHONE:
jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));
break;
case EMAIL:
jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));
break;
default:
throw new IllegalArgumentException("unknown privacy type enum " + privacyTypeEnum);
}
}
}
@Override
public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
final BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
if (privacyEncrypt == null) {
privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if (privacyEncrypt != null) {
return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),
privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}
实际使用
基于jackson,通过自定义注解的方式实现数据脱敏
添加依赖
spring-web、spring-boot-starter-web已经集成了jackson相关包,不用添加
<!--jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
脱敏注解
DesensitizationJsonSerializer.class: 脱敏序列化类
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationJsonSerializer.class)
public @interface Desensitization {
Class<? extends AbstractDesensitization> value();
}
脱敏序列化
/**
* 脱敏序列化
*/
public class DesensitizationJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private AbstractDesensitization desensitization;
public DesensitizationJsonSerializer() {
}
public DesensitizationJsonSerializer(AbstractDesensitization desensitization) {
this.desensitization = desensitization;
}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(desensitization.serialize(s));;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
JsonSerializer<?> jsonSerializer = null;
if(null == beanProperty) jsonSerializer = serializerProvider.findNullValueSerializer(beanProperty);
if(!Objects.equals(beanProperty.getType().getRawClass(), String.class))
jsonSerializer = serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
if(Objects.equals(beanProperty.getType().getRawClass(), String.class)){
jsonSerializer = setDesensitization(jsonSerializer, beanProperty);
}
return jsonSerializer;
}
/**
* 设置脱敏
* @param beanProperty
* @return
*/
private JsonSerializer<?> setDesensitization(JsonSerializer<?> jsonSerializer, BeanProperty beanProperty) {
Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
if (desensitization == null) desensitization = beanProperty.getContextAnnotation(Desensitization.class);
if (desensitization != null) {
//设置脱敏实例
try {
jsonSerializer = new DesensitizationJsonSerializer(desensitization.value().newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return jsonSerializer;
}
}
脱敏类
脱敏父类
子类通过继承AbstractDesensitization实现扩展
public abstract class AbstractDesensitization {
/**
* 脱敏
* @param value
* @return
*/
public abstract String serialize(String value);
}
中文姓名脱敏
public class ChineseNameDesensitization extends AbstractDesensitization {
@Override
public String serialize(String value) {
String serializeValue = "";
if(value.length() < 3){
serializeValue = value.replaceAll(".*(?=[\\u4e00-\\u9fa5])","*");
}else{
serializeValue = value.replaceAll("(?<=[\\u4e00-\\u9fa5]).*(?=[\\u4e00-\\u9fa5])","*");
}
return serializeValue;
}
}
手机号脱敏
public class MobilePhoneDesensitization extends AbstractDesensitization {
@Override
public String serialize(String value) {
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})","$1****$2");
}
}
身份证脱敏
public class IdCardDesensitization extends AbstractDesensitization {
@Override
public String serialize(String value) {
return value.replaceAll("(?<=\\w{3})\\w(?=\\w{4})","*");
}
}
测试
添加脱敏注解
public class User {
@Desensitization(ChineseNameDesensitization.class)
private String name;
private Integer age;
@Desensitization(IdCardDesensitization.class)
private String idCard;
@Desensitization(MobilePhoneDesensitization.class)
private String mobilePhone;
//...get and set
}
新建UserController,查询用户信息
@RestController
public class UserController {
@GetMapping("/users")
private List<User> users() throws Exception {
List<User> girls = new ArrayList<>();
User user = new User();
user.setName("西施");
user.setAge(18);
user.setIdCard("123456789123456202");
user.setMobilePhone("12345678901");
User user2 = new User();
user2.setName("杨贵妃");
user2.setAge(18);
user2.setIdCard("123456789123456202");
user2.setMobilePhone("12345678901");
User user3 = new User();
user3.setName("古代四大美女之一 * 貂蝉");
user3.setAge(18);
user3.setIdCard("123456789123456202");
user3.setMobilePhone("12345678901");
User user4 = new User();
user4.setName("古代四大美女之一 * 王昭君");
user4.setAge(18);
user4.setIdCard("123456789123456202");
user4.setMobilePhone("12345678901");
User user5 = new User();
user5.setName(null);
user5.setAge(18);
user5.setIdCard(null);
user5.setMobilePhone(null);
girls.add(user);
girls.add(user2);
girls.add(user3);
girls.add(user4);
girls.add(user5);
return girls;
}
}