數據脫敏又稱數據去隱私化或數據變形,是在給定的規則、策略下對敏感數據進行變換、修改的技術機制,可以在很大程度上解決敏感數據在非可信環境中使用的問題。根據數據保護規範和脫敏策略.對業務數據中的敏感信息實施自動變形.實現對敏感信息的隱藏。java
項目是在controller層進行脫敏,查閱google和github有兩種較爲方便的方法git
新建枚舉類型,標明是何種脫敏方式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; }
建立註解,標識須要脫敏的字段web
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
方法中出現遞歸調用致使棧溢出,具體緣由不明apache
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 { /** * 用序列化-反序列化方式實現深克隆 * 缺點:一、被拷貝的對象必需要實現序列化 * * @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; } /** * 用序列化-反序列化的方式實現深克隆 * 缺點:一、當實體中存在接口類型的參數,而且這個接口指向的實例爲枚舉類型時,會報錯"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; } }
建立脫敏工具類json
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
等方法