Java实现日志脱敏处理
题记
在日常工作中,日志处理是我们每一个程序员必备的素质,但是在有些场景下客户信息敏感,需要进行某些字段,或者某部分字段的脱敏处理。接到需求我们开始操刀!
需求分析
处理字段的方式多种多样,如何方便,高效才是关键,众所周知在java中最好的处理方式就是封装,即,对程序员暴露出的最好是一个统一的API,不关心具体的处理逻辑,能拿到想要的返回值就好。
实现第一版
由于在RPC调用过程当中,大部分接口的参数封装数据类型都是Map,所以在此先针对Map形式实现日志脱敏功能
实现思路:
有两种实现方法:
第一种:写死配置
第二种:使用注解驱动
由于写死配置的扩展性实在是差,所以我们本次实现主要是注解驱动
定义注解
/**
* @ClassName: DesensitizedAnnotation
* @Description: 注解类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:42
* @Version: 1.0
*/
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DesensitizedAnnotation {
/*脱敏数据类型(规则)*/
TypeEnum type();
/*判断注解是否生效,暂时没有用到*/
String isEffictiveMethod() default "";
}
引入枚举
主要是便于统一处理同类型的字段
public enum TypeEnum {
/**客户名称**/
PERSON_NAME,
/**客户证件号**/
PERSON_CERT_NO,
/**客户手机号**/
PERSON_PHONE_NO,
/**客户银行卡名称**/
PERSON_BANK_NAME,
/**客户银行卡号**/
PERSON_BANK_NO,
/**密码**/
PASSWORD,
}
定义基本数据模板类
主要作用是定义待过滤字段集合
/**
* @ClassName: BaseInfo
* @Description: 日志过滤字段基类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:38
* @Version: 1.0
*/
public class BaseInfo implements Serializable {
private static final long serialVersionUID = 1L;
@DesensitizedAnnotation(type = TypeEnum.PERSON_NAME)
private String custName;
@DesensitizedAnnotation(type = TypeEnum.PERSON_CERT_NO)
private String certNo;
}
定义处理工具类
/**
* @ClassName: DesensitizedUtils
* @Description: 日志脱敏工具类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:52
* @Version: 1.0
*/
public class DesensitizedUtils {
private static final Logger logger = LoggerFactory.getLogger(DesensitizedUtils.class);
private static final Map annotationMaps = new HashMap<>();
/**
* 类加载时装配待脱敏字段
*/
static {
try {
Class> clazz = Class.forName(BaseInfo.class.getName());
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
DesensitizedAnnotation annotation = fields[i].getAnnotation(DesensitizedAnnotation.class);
if (annotation != null) {
TypeEnum type = annotation.type();
String name = fields[i].getName();
//name为注解字段名称,value为注解类型。方便后续根据注解类型扩展
annotationMaps.put(name, type);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.error("类加载时装配待脱敏字段异常,异常信息:[{}]", new Object[]{e});
}
}
/**
* 脱敏处理方法
*
* @param object
* @return
*/
public static String getConverent(Map object) {
try {
// 1.处理Map数据类型
if (object instanceof Map) {
HashMap reqMap = (HashMap) object;
Iterator iterator = annotationMaps.keySet().iterator();
iterator.forEachRemaining(annotationName -> {
if (reqMap.keySet().contains(annotationName)) {
doconverentForMap(reqMap, annotationName);
}
});
return JSON.toJSONString(reqMap);
}
return JSON.toJSONString(object);
} catch (Exception e) {
e.printStackTrace();
logger.error("日志脱敏处理失败,回滚,详细信息:[{}]", new Object[]{e});
return JSON.toJSONString(object);
}
}
/**
* 脱敏数据源为Map时处理方式
*
* @param reqMap
* @param annotationName
* @return
*/
private static void doconverentForMap(HashMap reqMap, String annotationName) {
String value = String.valueOf(reqMap.get(annotationName));
if (StringUtils.isNotEmpty(value)) {
value = doConverentByType(value, annotationName);
}
reqMap.put(annotationName, value);
}
/**
* 根据不同注解类型处理不同字段
*
* @param value
* @param annotationName
* @return
*/
private static String doConverentByType(String value, String annotationName) {
TypeEnum typeEnum = annotationMaps.get(annotationName);
switch (typeEnum) {
case PERSON_NAME:
value = getStringByLength(value);
break;
case PERSON_CERT_NO:
value = getStringByLength(value);
default:
value = getStringByLength(value);
}
return value;
}
/**
* 根据value长度取值(切分)
*
* @param value
* @return
*/
private static String getStringByLength(String value) {
int length = value.length();
if (length == 2){
value = value.substring(0, 1) + "*";
}else if (length == 3){
value = value.substring(0,1) + "*" + value.substring(length -1);
}else if (length > 3 && length <= 5){
value = value.substring(0,1) + "**" + value.substring(length -2);
}else if (length > 5 && length <= 7){
value = value.substring(0,2) + "***" + value.substring(length -2);
}else if (length > 7){
value = value.substring(0,3) + "*****" + value.substring(length -3);
}
return value;
}
}
定义测试类
测试第一版实现的针对Map处理的脱敏操作
/**
* @ClassName: TestDeaensitized
* @Description: 日志脱敏测试类
* @Author: 尚先生
* @CreateDate: 2019/1/24 18:27
* @Version: 1.0
*/
public class TestDeaensitized {
public static void main(String[] args) {
HashMap hashMap = new HashMap<>();
hashMap.put("custName", "小妮儿");
hashMap.put("certNo", "12345678909876543");
hashMap.put("phone", "12345678909");
System.out.println("脱敏前:" + hashMap);
String converent1 = DesensitizedUtils.getConverent(hashMap);
System.out.println("脱敏后:" + converent1);
}
}
第一版实现测试结果
针对Map实现的脱敏结果
脱敏前:{certNo=12345678909876543, phone=12345678909, custName=小妮儿}
脱敏后:{"certNo":"123*****543","phone":"12345678909","custName":"小*儿"}
至此第一版功能实现顺利完成。
实现第二版
由于在RPC调用过程当中,大部分接口的参数封装数据类型都是Map,但是部分接口还是使用Java Bean所以在此针对Java Bean形式实现日志脱敏功能
实现思路:
根据不同的数据类型进行不同判断,屏蔽上层调用者的可见度,在底层动态实现分情况处理
在结果处理完之后,统一返回调用者序列化完成的数据信息
在第一版实现的基础之上,我们开始第二版的实现
添加实体类
主要是为了封装模拟RPC调用过程中参数实体的属性
/**
* @ClassName: Person
* @Description: Person实体类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:50
* @Version: 1.0
*/
public class Person {
private String custName;
private int idNo;
private String certNo;
public String getCustName() {
return custName;
}
public void setCustName(String custName) {
this.custName = custName;
}
public int getIdNo() {
return idNo;
}
public void setIdNo(int idNo) {
this.idNo = idNo;
}
public String getCertNo() {
return certNo;
}
public void setCertNo(String certNo) {
this.certNo = certNo;
}
@Override
public String toString() {
return "Person{" +
"custName='" + custName + '\'' +
", idNo=" + idNo +
", certNo='" + certNo + '\'' +
'}';
}
}
改造处理工具类
/**
* @ClassName: DesensitizedUtils
* @Description: 日志脱敏工具类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:52
* @Version: 1.0
*/
public class DesensitizedUtils {
private static final Logger logger = LoggerFactory.getLogger(DesensitizedUtils.class);
private static final Map annotationMaps = new HashMap<>();
/**
* 类加载时装配待脱敏字段
*/
static {
try {
Class> clazz = Class.forName(BaseInfo.class.getName());
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
DesensitizedAnnotation annotation = fields[i].getAnnotation(DesensitizedAnnotation.class);
if (annotation != null) {
TypeEnum type = annotation.type();
String name = fields[i].getName();
//name为注解字段名称,value为注解类型。方便后续根据注解类型扩展
annotationMaps.put(name, type);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.error("类加载时装配待脱敏字段异常,异常信息:[{}]", new Object[]{e});
}
}
/**
* 脱敏处理方法
*
* @param object
* @return
*/
public static String getConverent(Object object) {
String objClassName = object.getClass().getName();
try {
// 1.处理Map数据类型
if (object instanceof Map) {
HashMap reqMap = (HashMap) object;
Iterator iterator = annotationMaps.keySet().iterator();
iterator.forEachRemaining(annotationName -> {
if (reqMap.keySet().contains(annotationName)) {
doconverentForMap(reqMap, annotationName);
}
});
return JSON.toJSONString(reqMap);
}
// 2.处理Object数据类型
Object val = new Object();
Class> objClazz = Class.forName(objClassName);
Field[] declaredFields = objClazz.getDeclaredFields();
for (int j = 0; j < declaredFields.length; j++) {
Iterator iterator = annotationMaps.keySet().iterator();
while (iterator.hasNext()) {
String annotationName = iterator.next();
if (declaredFields[j].getName().equals(annotationName)) {
declaredFields[j].setAccessible(true);
val = declaredFields[j].get(object);
//获取属性后现在默认处理的是String类型,其他类型数据可扩展
String value = doconverentForObject(val, annotationName);
declaredFields[j].set(object, value);
}
}
}
return JSON.toJSONString(object);
} catch (Exception e) {
e.printStackTrace();
logger.error("日志脱敏处理失败,回滚,详细信息:[{}]", new Object[]{e});
return JSON.toJSONString(object);
}
}
/**
* 脱敏数据源为Object时处理方式
*
* @param val
* @param annotationName
* @return
*/
private static String doconverentForObject(Object val, String annotationName) {
String value = String.valueOf(val);
if (StringUtils.isNotEmpty(value)) {
value = doConverentByType(value, annotationName);
}
return value;
}
/**
* 脱敏数据源为Map时处理方式
*
* @param reqMap
* @param annotationName
* @return
*/
private static void doconverentForMap(HashMap reqMap, String annotationName) {
String value = String.valueOf(reqMap.get(annotationName));
if (StringUtils.isNotEmpty(value)) {
value = doConverentByType(value, annotationName);
}
reqMap.put(annotationName, value);
}
/**
* 根据不同注解类型处理不同字段
*
* @param value
* @param annotationName
* @return
*/
private static String doConverentByType(String value, String annotationName) {
TypeEnum typeEnum = annotationMaps.get(annotationName);
switch (typeEnum) {
case PERSON_NAME:
value = getStringByLength(value);
break;
case PERSON_CERT_NO:
value = getStringByLength(value);
default:
value = getStringByLength(value);
}
return value;
}
/**
* 根据value长度取值(切分)
*
* @param value
* @return
*/
private static String getStringByLength(String value) {
int length = value.length();
if (length == 2){
value = value.substring(0, 1) + "*";
}else if (length == 3){
value = value.substring(0,1) + "*" + value.substring(length -1);
}else if (length > 3 && length <= 5){
value = value.substring(0,1) + "**" + value.substring(length -2);
}else if (length > 5 && length <= 7){
value = value.substring(0,2) + "***" + value.substring(length -2);
}else if (length > 7){
value = value.substring(0,3) + "*****" + value.substring(length -3);
}
return value;
}
}
定义测试类
测试第二版实现的针对Object处理的脱敏操作
/**
* @ClassName: TestDeaensitized
* @Description: 日志脱敏测试类
* @Author: 尚先生
* @CreateDate: 2019/1/24 18:27
* @Version: 1.0
*/
public class TestDeaensitized {
public static void main(String[] args) {
HashMap hashMap = new HashMap<>();
hashMap.put("custName", "小妮儿");
hashMap.put("certNo", "12345678909876543");
hashMap.put("phone", "12345678909");
System.out.println("脱敏前:" + hashMap);
String converent1 = DesensitizedUtils.getConverent(hashMap);
System.out.println("脱敏后:" + converent1);
Person person = new Person();
person.setCertNo("12345678909876541");
person.setCustName("小妮儿真可爱!");
System.out.println("脱敏前:" + person);
String converent2 = DesensitizedUtils.getConverent(person);
System.out.println("脱敏后:" + converent2);
}
第二版实现测试结果
针对Map实现的脱敏结果
脱敏前:{certNo=12345678909876543, phone=12345678909, custName=小妮儿}
脱敏后:{"certNo":"123*****543","phone":"12345678909","custName":"小*儿"}
针对Object实现的脱敏结果
脱敏前:Person{custName='小妮儿真可爱!', idNo=0, certNo='12345678909876541'}
脱敏后:{"certNo":"123*****541","custName":"小妮***爱!","idNo":0}
至此所有功能实现顺利完成。
完整代码和相关依赖请见GitHub
https://github.com/dwyanewede/project-learn/tree/master/src/main/java/com/learn/demo/desensitization
微信公众号
java界的小学生