數據脫敏
數據脫敏又稱數據去隱私化或數據變形,是在給定的規則、策略下對敏感數據進行變換、修改的技術機制,能夠在很大程度上解決敏感數據在非可信環境中使用的問題。根據數據保護規范和脫敏策略.對業務數據中的敏感信息實施自動變形.實現對敏感信息的隱藏。
脫敏方法
項目是在controller層進行脫敏,查閱google和github有兩種較為方便的方法
- 一種是基於注解 desensitized基於注解的形式進行脫敏 GitHub
- 一種是基於框架本質上還是注解,但是已經封裝好了,當然還提供fastjson的方式進行脫敏 sensitive框架進行脫敏 GitHub
由於項目原因不想引入框架,所以使用基於注解的方式
實現
新建枚舉類型,標明是何種脫敏方式
package com.blgroup.vision.common.enums;
/**
* @program: blgroup-scloud-web
* @description: 脫敏枚舉
* @author: ingxx
* @create: 2019-12-17 14:32
**/
public enum SensitiveTypeEnum {
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份證號
*/
ID_CARD,
/**
* 座機號
*/
FIXED_PHONE,
/**
* 手機號
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 電子郵件
*/
EMAIL,
/**
* 銀行卡
*/
BANK_CARD,
/**
* 虛擬賬號
*/
ACCOUNT,
/**
* 密碼
*/
PASSWORD;
}
創建注解,標識需要脫敏的字段
package com.blgroup.vision.common.annotation;
import com.blgroup.vision.common.enums.SensitiveTypeEnum;
import java.lang.annotation.*;
/**
* @program: blgroup-scloud-web
* @description: 脫敏注解
* @author: ingxx
* @create: 2019-12-17 14:32
**/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Desensitized {
/*脫敏類型(規則)*/
SensitiveTypeEnum type();
/*判斷注解是否生效的方法*/
String isEffictiveMethod() default "";
}
創建Object工具類,用於復制對象和對對象的其他操作,注意使用fastjson實現深拷貝對於復雜的對象會出現棧溢出,本人測試是發生在轉換對象時對象會轉換成JsonObject對象,在getHash
方法中出現遞歸調用導致棧溢出,具體原因不明
package com.blgroup.vision.common.utils;
import com.alibaba.fastjson.JSON;
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.*;
/**
* @program: blgroup-scloud-web
* @description: 脫敏Object工具類
* @author: ingxx
* @create: 2019-12-17 14:32
**/
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;
}
/**
* 用序列化-反序列化的方式實現深克隆
* 缺點: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());
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.blgroup.vision.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.blgroup.vision.common.annotation.Desensitized;
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.*;
/**
* @program: blgroup-scloud-web
* @description: 脫敏工具類
* @author: ingxx
* @create: 2019-12-17 14:32
**/
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);
/* 清空計數器 */
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;
}
}
效果展示
因生產項目中不能展示 所以采用Demo的形式
創建測試實體類
package top.ingxx.pojo;
import lombok.Data;
import top.ingxx.annotation.Desensitized;
import top.ingxx.enums.SensitiveTypeEnum;
@Data
public class UserInfo {
@Desensitized(type = SensitiveTypeEnum.CHINESE_NAME)
private String realName;
@Desensitized(type = SensitiveTypeEnum.ID_CARD)
private String idCardNo;
@Desensitized(type = SensitiveTypeEnum.MOBILE_PHONE)
private String mobileNo;
@Desensitized(type = SensitiveTypeEnum.ADDRESS)
private String address;
@Desensitized(type = SensitiveTypeEnum.PASSWORD)
private String password;
@Desensitized(type = SensitiveTypeEnum.BANK_CARD)
private String bankCard;
}
測試實體字段
package top.ingxx.pojo;
import lombok.Data;
/**
* @program: demo
* @description: 測試
* @author: ingxx
* @create: 2019-12-18 09:16
**/
@Data
public class Test {
private UserInfo userInfo;
}
測試主方法
import com.alibaba.fastjson.JSON;
import top.ingxx.pojo.Test;
import top.ingxx.pojo.UserInfo;
import top.ingxx.utils.DesensitizedUtils;
public class Main {
public static void main(String[] args) {
UserInfo userInfo = new UserInfo();
userInfo.setAddress("上海浦東新區未知地址12號");
userInfo.setIdCardNo("111521444474441411");
userInfo.setMobileNo("13555551141");
userInfo.setPassword("123456");
userInfo.setRealName("張三");
userInfo.setBankCard("111521444474441411");
Test test = new Test();
test.setUserInfo(userInfo);
System.out.println(JSON.toJSONString(test));
Test obj = DesensitizedUtils.getObj(test);
System.out.println(JSON.toJSONString(obj));
}
}
使用時可以對單個字段脫敏 也可以直接使用 getObj
getJson
getList
等方法