由于最近需要实现一个日志脱敏,而公司的日志API是公司自己封装的,不好扩展,于是想从打印的内容上想点办法。
我们的通讯报文一般是JSON格式的,或者XML格式的,于是,只要打印的时候,创建打印对象的副本,在副本的基础上进行字段屏蔽,然后再打印该副本。
首先,我们得制定一个脱敏规则,这个规则我这里放在配置文件里面,desensitization.properties
格式就是 [脱敏字段名]=[规则1],[规则2]
下面是样例文件:
#index从1开始,请勿输0
password=1-3,6-7
username=4-8
telephone=4-8
idCard=7-100
思路:
我的思路很简单,就是递归遍历JSONObject或者XML的Document对象,然后遍历时,查找这个字段是否是脱敏规则里的字段,如果是,就按照规则替换部分字符串。
然后就是代码实现了,导入两个包,一个是FastJSON的包另外一个是Dom4j的包,
直接贴代码了:
JSONUtils.java 用于处理JSON字段的脱敏
package com.hgd;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
public class JSONUtils {
//用于保存字段屏蔽的规则
private static Map<String,List<RulesItem>> rules;
//屏蔽规则提前加载到内存
static {
rules = new HashMap<>();
//password=1-2,5-6
Map<String,String> map = loadProperties(JSONUtils.class.getClassLoader().getResource("").getPath()+"/desensitization.properties");
map.forEach((key,value)->{
String[] propertyRules = value.split(",");
List<RulesItem> arrays = new ArrayList<>(propertyRules.length);
for (String propertyRule : propertyRules) {
String[] tempArgs = propertyRule.split("-");
RulesItem rulesArray = new RulesItem();
int start = Integer.parseInt(tempArgs[0]);
int end = Integer.parseInt(tempArgs[1]);
rulesArray.setStart(start);
rulesArray.setEnd(end);
arrays.add(rulesArray);
}
rules.put(key,arrays);
});
System.out.println("rules"+ rules);
}
/**
* 将JSONObject 转化成我们自定义的CustomerJSON
* @param jsonObject 原始json
* @return
*/
public static CustomerJson toCustomerJson(JSONObject jsonObject){
return new CustomerJson(jsonObject);
}
/**
* 递归遍历JSONObject
* @param object
* @return
*/
public static JSONObject changeJSONObject(JSONObject object){
JSONObject result = new JSONObject(object.size());
object.forEach((key,value)->{
if(value instanceof JSONObject){
result.put(key,changeJSONObject((JSONObject) value));
}else if(value instanceof JSONArray){
result.put(key,changeJSONArray((JSONArray)value));
}else{
//这里进行字段脱敏
result.put(key,desensitization(key,String.valueOf(value)));
}
});
return result;
}
/**
* 递归遍历JOSNArray
* @param jsonArray
* @return
*/
public static JSONArray changeJSONArray(JSONArray jsonArray){
if(jsonArray!=null){
JSONArray array = new JSONArray(jsonArray.size());
jsonArray.forEach(object->{
if(object instanceof JSONObject){
array.add(changeJSONObject((JSONObject)object));
}else{
array.add(object);
}
});
return array;
}else {
return null;
}
}
/**
* 对字段进行脱敏
* @param key 字段名称
* @param value 字段值
* @return
*/
public static String desensitization(String key,String value){
if(null == value || "".equals(value)){
return value;
}
if(rules.containsKey(key)){
List<RulesItem> arrays = rules.get(key);
for (RulesItem temp : arrays) {
value = desensiString(value, temp);
}
}
return value;
}
/**
* 对原始字符串进行替换操作
* @param value 字段值
* @param item 屏蔽规则
* @return 屏蔽后的字段值
*/
public static String desensiString(String value, RulesItem item){
StringBuilder sb = new StringBuilder(value);
int length = value.length();
int filledLength = item.getEnd() - item.getStart() + 1 ;
//保护,当问文件中的配置不是正数时,赋默认值1
if(item.getStart()<=0){
item.setStart(1);
}
if(item.getEnd() <=0){
item.setEnd(1);
}
//保护,当原始字符串太短时,特殊处理,不处理,或者全部替换成*;
if(item.getStart()>sb.length()){
//return sb.toString();
return getFilledString(‘*‘,sb.length());
}
System.out.println("start:"+(item.getStart()-1)+"end:"+(item.getEnd()-1));
sb.replace(item.getStart()-1,item.getEnd()-1,getFilledString(‘*‘, Math.min(filledLength, length)));
return sb.toString();
}
/**
* 生成固定长度的填充串
* @param filledChar 填充串的字符
* @param length 填充串的长度
* @return 生成好的字符串
*/
public static String getFilledString(char filledChar,int length){
StringBuilder sb = new StringBuilder();
for (int i= 0 ;i<length;i++){
sb.append(filledChar);
}
return sb.toString();
}
/**
* 读取配置文件
* @param path 配置文件路径
* @return
*/
public static Map<String,String> loadProperties(String path){
Properties properties = new Properties();
try {
properties.load(new FileInputStream(new File(path)));
} catch (IOException e) {
e.printStackTrace();
}
Map<String,String> result = new HashMap<>(properties.size());
Set<Map.Entry<Object, Object>> entrySet = properties.entrySet();
for (Map.Entry<Object, Object> entry : entrySet) {
result.put((String) entry.getKey(), (String) entry.getValue());
}
return result;
}
}
XMLUtils.java 用于处理XML格式的报文脱敏
package com.hgd;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.util.List;
public class XMLUtils {
public static Element getNodes(Element node) {
List<Element> listElement = node.elements();
Element result = DocumentHelper.createElement(node.getName());
if (listElement == null || listElement.size() == 0) {//
System.out.println(node.getTextTrim());
//这里进行特定的修改
Element element = DocumentHelper.createElement(node.getName());
element.setText(JSONUtils.desensitization(node.getName(), node.getTextTrim()));
return element;
} else {
//所有一级子节点的list
for (Element e : listElement) {//遍历所有一级子节点
result.add(getNodes(e));
}
}
return result;
}
}
脱敏规则的原子类,用于存储单个字段的脱敏规则
RulesItem.java
package com.hgd;
public class RulesItem {
private int start;
private int end;
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
@Override
public String toString() {
return "RulesItem{" +
"start=" + start +
", end=" + end +
‘}‘;
}
}
最终写个测试类,测试一下:
Main.java
package com.hgd;
import com.alibaba.fastjson.JSON;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
public class Main {
public static void main(String[] args) {
String oriJSON = "{\n" +
" \"username\":\"abcdefghijklmn\",\n" +
" \"password\":\"helloWorld\",\n" +
" \"telephone\":\"17789098907\",\n" +
" \"idCard\":\"441279199012043817\",\n" +
" \"innerUser\":[\n" +
" {\n" +
" \"telephone\":\"18979089876\"\n" +
" }\n" +
" ]\n" +
"}";
System.out.println(JSONUtils.changeJSONObject(JSON.parseObject(oriJSON)).toJSONString());
String utils = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<root>\n" +
"\t<user editor=\"chenleixing\" date=\"2015-02-15\">\n" +
"\t\t<username>张三</username>\n" +
"\t\t<telephone>17774570620</telephone>\n" +
"\t\t<sex>男</sex>\n" +
"\t</user>\n" +
"\t<user editor=\"zhangxiaochao\" date=\"2015-02-15\">\n" +
"\t\t<name>李四</name>\n" +
"\t\t<year>24</year>\n" +
"\t\t<sex>女</sex>\n" +
"\t</user>\n" +
"</root>";
try {
Document document = DocumentHelper.parseText(utils);
Element result = XMLUtils.getNodes(document.getRootElement());
Document resultDoc = DocumentHelper.createDocument(result);
System.out.println(resultDoc.asXML());
} catch (DocumentException e) {
e.printStackTrace();
}
}
}
输出结果:
rules{password=[RulesItem{start=1, end=3}], idCard=[RulesItem{start=7, end=100}], telephone=[RulesItem{start=4, end=8}], username=[RulesItem{start=4, end=8}]}
start:0end:2
start:6end:99
start:3end:7
start:3end:7
start:3end:7
{"innerUser":[{"telephone":"189*****9876"}],"password":"***lloWorld","telephone":"177*****8907","idCard":"441279******************","username":"abc*****hijklmn"}
张三
17774570620
start:3end:7
男
李四
24
女
<?xml version="1.0" encoding="UTF-8"?>
<root><user><username>**</username><telephone>177*****0620</telephone><sex>男</sex></user><user><name>李四</name><year>24</year><sex>女</sex></user></root>