一、需求背景
日常工作中,必不可免的会将一些敏感信息,如用户名、密码、手机号、身份证号、银行账号等等打印出来,但往往为了安全,这些信息都需要进行脱敏。脱敏实际就是用一些特殊字符来替换部分值。
JSON 和 JSONObject
Fastjson 是阿里巴巴开源的一个高性能 JSON 库,其中 JSON 和 JSONObject 是两个常用但功能有所不同的类。JSON 类主要用于序列化和反序列化操作,而 JSONObject 则是用于直接操作 JSON 数据结构
二、基础数据
maven导入依赖
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
UserVO类
public class UserVO {
//用户名
private String username;
//密码
private String password;
//身份证号
private String certNo;
//地址
private String address;
//工作年限
private Integer workYear;
//电话号码
private String phone;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCertNo() {
return certNo;
}
public void setCertNo(String certNo) {
this.certNo = certNo;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getWorkYear() {
return workYear;
}
public void setWorkYear(Integer workYear) {
this.workYear = workYear;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
logback.xml配置文件,之前一篇文章(Logback的使用-CSDN博客)分享过配置文件的各标签含义,这里就不做重复介绍,重点是将日志脱敏。
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- property指定日志输出格式,他有两个属性:
name定义property节点的名称,value属性设置具体的日志输出格式
property节点定义之后,下面的节点可以直接使用“${}”来引用value中定义的日志输出格式
-->
<!--
日志输出格式:
%-5level %level表示日志级别,-5表示占5个字符,如果不足,就向左对齐
%d{yyyy-mm-dd H:mm:ss.sss} %d表示日期,后面是日期的格式
%c 表示 类的完整名称
%M 表示 method
%L 表示 行号
%thread 表示 线程名称
%m或者%msg 表示 信息
%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出
%logger{36} 表示 使用哪个日志记录器,就会打印那个日志记录器的name,最多显示36个字符
%n 表示 换行
被[]中括号括起来,只是为了方便区分,也可以将中括号去掉,不会有影响
-->
<!--%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出-->
<property name="NEW_LOG_STYLE"
value="[%-5level] [%thread] [%logger] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n"/>
<conversionRule conversionWord="m" converterClass="cn.tedu.TuoMinConverter"/>
<conversionRule conversionWord="msg" converterClass="cn.tedu.TuoMinConverter"/>
<!-- 控制台输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--encoder指定日志格式,class属性可以不写,默认会将值映射到PatternLayoutEncoder的变量中-->
<!-- <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> -->
<encoder>
<!--使用上文定义的,全局的property配置-->
<pattern>${NEW_LOG_STYLE}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
三、基于logback的MessageConverter和JSONObject,进行全局脱敏
定义一个工具类JSONUtils,用于脱敏
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.Iterator;
public class JSONUtils {
/**
* 更新json数据
*
* @param objJson
* @param nodeKey 需要脱敏的节点名
* @param maskValue
* @return
*/
public static Object updateJson(Object objJson, String nodeKey, String maskValue) {
//如果传入的是json数组或者json对象,需要进行递归
if (objJson instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) objJson;
for (int i = 0; i < jsonArray.size(); i++) {
updateJson(jsonArray.get(i), nodeKey, maskValue);
}
} else if (objJson instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) objJson;
Iterator<String> iterator = jsonObject.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next().toString();
Object obj = jsonObject.get(key);
if (obj instanceof JSONArray) {
updateJson(obj, nodeKey, maskValue);
} else if (obj instanceof JSONObject) {
updateJson(obj, nodeKey, maskValue);
} else {
//说明已经递归到最底层,开始判断key是否需要掩码
if (key.equals(nodeKey)) {
jsonObject.put(key, maskValue);
}
}
}
}
return objJson;
}
}
四、自定义一个转换器TuoMinConverter
TuoMinConverter 继承 logback中的MessageConverter类
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.helpers.MessageFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class TuoMinConverter extends MessageConverter {
private static final String[] maskParams = {"password","certNo", "phone"};
@Override
public String convert(ILoggingEvent event) {
try {
return doTuoMin(event);
} catch (Exception e) {
return super.convert(event);
}
}
private String doTuoMin(ILoggingEvent event) {
try {
Object[] objects = Stream.of(event.getArgumentArray()).map(obj -> {
String msg;
if (obj instanceof String) {
msg = obj.toString();
} else {
msg = maskJson(JSON.toJSONString(obj));
}
return msg;
}).toArray();
//将{}占位符的内容,替换为objects数组中的数据
return MessageFormatter.arrayFormat(event.getMessage(), objects).getMessage();
} catch (Exception e) {
return event.getMessage();
}
}
/**
* 脱敏
*
* @return 返回脱敏之后的json串
*/
private static String maskJson(String jsonStr) {
Object maskString = null;
List<String> marks = isMark(jsonStr);
if (!marks.isEmpty()) {
for (String mark : marks) {
maskString = JSONUtils.updateJson(JSONObject.parseObject(jsonStr), mark, "*****");
//这句代码是必须的,保证存在多个需要脱敏的字段时,不会将之前已经脱敏的字段给还原
jsonStr = maskString.toString();
}
return maskString.toString();
}else {
//说明日志不需要脱敏
return jsonStr;
}
}
/**
* 判断传入的字符串中是否包含需要脱敏的字段
*
* @param value
* @return 返回需要脱敏的字段集合
*/
private static List<String> isMark(String value) {
List<String> maskParamList = new ArrayList<>();
for (String s : Arrays.asList(maskParams)) {
if (value.contains(s)) {
maskParamList.add(s);
}
}
return maskParamList;
}
}
logback.xml配置文件中声明这个转换器
我这里是在配置文件中定义了两个转换器,conversionWord是转换词,当识别到这个转换词之后,才会走自定义的转换器。我配置文件中的日志输出格式是用 [%m],因此,成功匹配第一个转换器。
<conversionRule conversionWord="m" converterClass="cn.tedu.TuoMinConverter"/>
<conversionRule conversionWord="msg" converterClass="cn.tedu.TuoMinConverter"/>
创建一个启动类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo {
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
public static void main(String[] args) throws Exception {
UserVO vo = new UserVO();
vo.setUsername("UMR");
vo.setPassword("12345678");
vo.setCertNo("4008123123");
vo.setWorkYear(3);
vo.setAddress("北京");
vo.setPhone("13608731439");
logger.info("查询到的员工信息:{}", vo);
logger.info("耗时:{} 毫秒", 5);
}
}
运行,打印结果如下:
五、全局脱敏2.0版,使用Fastjson的值过滤器
上面这种脱敏方式,难点在于对JSON数据进行递归。针对这种情况,可以使用fastjson的值过滤器,实现在序列化的时候,对指定字段的值进行修改,保证最后的json串符合预期。
自定义一个值过滤器 DefineJsonValueFilter
import com.alibaba.fastjson.serializer.ValueFilter;
public class DefineJsonValueFilter implements ValueFilter {
private static final String[] maskParams = {"password", "certNo", "phone"};
@Override
public Object process(Object object, String name, Object value) {
for (String maskParam : maskParams) {
//如果匹配到对应字段,就说明需要进行脱敏
if (maskParam.equalsIgnoreCase(name)) {
//返回脱敏后的值
return "***";
}
}
//说明不需要脱敏
return value;
}
}
对 TuoMinConverter类 进行调整,不再调用自定义的JSONUtils
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.alibaba.fastjson.JSON;
import org.slf4j.helpers.MessageFormatter;
import java.util.stream.Stream;
public class TuoMinConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event) {
try {
return doTuoMin(event);
} catch (Exception e) {
return super.convert(event);
}
}
private String doTuoMin(ILoggingEvent event) {
try {
Object[] objects = Stream.of(event.getArgumentArray()).map(obj -> {
String msg;
if (obj instanceof String) {
msg = obj.toString();
} else {
//序列化时,使用自定义的值过滤器
msg = JSON.toJSONString(obj, new DefineJsonValueFilter());
}
return msg;
}).toArray();
//将{}占位符的内容,替换为objects数组中的数据
return MessageFormatter.arrayFormat(event.getMessage(), objects).getMessage();
} catch (Exception e) {
return event.getMessage();
}
}
}
创建老师实体,成员变量包含学生信息
public class Teacher {
private String teaName;
private String phone;
//学生信息
private Student student;
private String address;
public Teacher(String teaName, String phone, Student student) {
this.teaName = teaName;
this.phone = phone;
this.student = student;
}
public String getTeaName() {
return teaName;
}
public void setTeaName(String teaName) {
this.teaName = teaName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
学生类实体
public class Student {
private String stuName;
private String phone;
private String address;
public Student(String stuName, String phone) {
this.stuName = stuName;
this.phone = phone;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
执行下面main方法
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo {
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
public static void main(String[] args) throws Exception {
Student stu = new Student("UMR", "408123123");
Teacher t = new Teacher("Mr.Hu", "123456789", stu);
logger.info("老师信息:{}", t);
logger.info("耗时:{} 毫秒", 5);
}
}
六、针对不同的字段,需要使用不同的脱敏规则
如果姓名要求保留第一个字符,手机号码要求保留前三和后四个字符,针对这种情况,就需要在值过滤器中进行特殊处理。
第一种方式是每出现一个字段需要按照特定的脱敏规则,就添加一个if。这种比较简单,但每新增一个字段的规则,就需要新增一个if。
这里介绍第二种,利用枚举,每新增一个字段的规则,只需要定义一个枚举就行,不需要每次都在值过滤器中添加一个if判断。
public enum DataMaskRule {
PHONE("手机号掩码,保留前三后四", "phone", "^(\\d{3})\\d+(\\d{4})$", "$1****$2"),
NAME("姓名掩码,保留开头第一位", "teaName|stuName", "(.{1})(.+)", "$1**");
DataMaskRule(String desc, String fieldName, String regex, String maskResult) {
this.desc = desc;
this.fieldName = fieldName;
this.regex = regex;
this.maskResult = maskResult;
}
/**
* 脱敏规则描述
*/
public String desc;
/**
* 要脱敏的属性名
*/
public String fieldName;
/**
* 正则表达式(要脱敏的属性,匹配指定正则,才进行脱敏)
*/
public String regex;
/**
* 脱敏结果
*/
public String maskResult;
}
调整自定义的值过滤器 DefineJsonValueFilter
import com.alibaba.fastjson.serializer.ValueFilter;
public class DefineJsonValueFilter implements ValueFilter {
@Override
public Object process(Object object, String name, Object value) {
for (DataMaskRule dataMaskRule : DataMaskRule.values()) {
for (String filed : dataMaskRule.fieldName.split("\\|")) {
if (filed.equalsIgnoreCase(name)) {
//如果匹配正则成功,将value按照枚举的maskResult定义的格式进行替换
return value.toString().replaceAll(dataMaskRule.regex, dataMaskRule.maskResult);
}
}
}
//说明不需要脱敏
return value;
}
}
执行下面main方法
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo {
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
public static void main(String[] args) throws Exception {
Student stu = new Student("李四", "408123123");
Teacher t = new Teacher("张三老师", "123456789", stu);
logger.info("老师信息:{}", t);
logger.info("耗时:{} 毫秒", 5);
}
}
结果如下