最近在研究如何使用logback实现日志脱敏的工作,网上各种查,各种找,终于找到了解决的办法。其实原理知道,就是想找一个最简便的方法而已。那今天咱们就来聊聊如何用Logback创建自定义格式转换符来实现日志脱敏**
- 脱敏类文件
- 配置文件
脱敏类文件
数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。 —— [ 百度百科 ]
本文使用最简单的脱敏方式进行数据脱敏打印,规则如下:
| 参数 | 脱敏前 | 脱敏后 |
| ——– | ——– | ——– |
| 姓名 | 李丽丽 | 李** |
| 手机号 | 13898701234 | 138****1234 |
| 身份证号 | 111111111111115762 | **************5762 |
| 银行卡号 | 6222600890987671234 | 6222600********1234 |
具体代码
首先定义类:SensitiveDataConverter 继承父类:MessageConverter
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 敏感信息脱敏处理
* @author AAA
*/
public class SensitiveDataConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event){
// 获取原始日志
String oriLogMsg = event.getFormattedMessage();
// 获取脱敏后的日志
String afterLogMsg = invokeMsg(oriLogMsg);
return afterLogMsg;
}
/**
* 日志脱敏开关
*/
private static String converterCanRun = "true";
/**
* 日志脱敏关键字
*/
private static String sensitiveDataKeys = "idcard,realname,bankcard,mobile";
/**
* 处理日志字符串,返回脱敏后的字符串
* @param msg
* @return
*/
public String invokeMsg(final String oriMsg){
String tempMsg = oriMsg;
if("true".equals(converterCanRun)){
// 处理字符串
if(sensitiveDataKeys != null && sensitiveDataKeys.length() > 0){
String[] keysArray = sensitiveDataKeys.split(",");
for(String key: keysArray){
int index= -1;
do{
index = tempMsg.indexOf(key, index+1);
if(index != -1){
// 判断key是否为单词字符
if(isWordChar(tempMsg, key, index)){
continue;
}
// 寻找值的开始位置
int valueStart = getValueStartIndex(tempMsg, index + key.length());
// 查找值的结束位置(逗号,分号)........................
int valueEnd = getValuEndEIndex(tempMsg, valueStart);
// 对获取的值进行脱敏
String subStr = tempMsg.substring(valueStart, valueEnd);
subStr = tuomin(subStr, key);
///
tempMsg = tempMsg.substring(0,valueStart) + subStr + tempMsg.substring(valueEnd);
}
}while(index != -1);
}
}
}
return tempMsg;
}
private static Pattern pattern = Pattern.compile("[0-9a-zA-Z]");
/**
* 判断从字符串msg获取的key值是否为单词 , index为key在msg中的索引值
* @return
*/
private boolean isWordChar(String msg, String key, int index){
// 必须确定key是一个单词............................
if(index != 0){ // 判断key前面一个字符
char preCh = msg.charAt(index-1);
Matcher match = pattern.matcher(preCh + "");
if(match.matches()){
return true;
}
}
// 判断key后面一个字符
char nextCh = msg.charAt(index + key.length());
Matcher match = pattern.matcher(nextCh + "");
if(match.matches()){
return true;
}
return false;
}
public static void main(String[] args) {
String tempMsg = "{sign=f88898b2677e62f1ad54b9e330c0a27e, idcard=130333198901192762, realname=%E5%BE%90%E5%BD%A6%E5%A8%9C, key=c5d34d4c3c71cc45c88f32b4f13da887, mobile=13210141605, bankcard=6226430106137525}";
String tempMsg1 = "{\"reason\":\"成功 \",\"result\":{\"jobid\":\"JH2131171027170837443588J6\",\"realname\":\"李哪娜\",\"bankcard\":\"6226430106137525\",\"idcard\":\"130333198901192762\",\"mobile\":\"13210141605\",\"res\":\"1\",\"message\":\"验证成功\"},\"error_code\":0}";
SensitiveDataConverter sc = new SensitiveDataConverter();
System.out.println(sc.invokeMsg(tempMsg));
System.out.println(sc.invokeMsg(tempMsg1));
}
/**
* 获取value值的开始位置
* @param msg 要查找的字符串
* @param valueStart 查找的开始位置
* @return
*/
private int getValueStartIndex(String msg, int valueStart ){
// 寻找值的开始位置.................................
do{
char ch = msg.charAt(valueStart);
if(ch == ':' || ch == '='){ // key与 value的分隔符
valueStart ++;
ch = msg.charAt(valueStart);
if(ch == '"'){
valueStart ++;
}
break; // 找到值的开始位置
}else{
valueStart ++;
}
}while(true);
return valueStart;
}
/**
* 获取value值的结束位置
* @return
*/
private int getValuEndEIndex(String msg,int valueEnd){
do{
if(valueEnd == msg.length()){
break;
}
char ch = msg.charAt(valueEnd);
if(ch == '"'){ // 引号时,判断下一个值是结束,分号还是逗号决定是否为值的结束
if(valueEnd+1 == msg.length()){
break;
}
char nextCh = msg.charAt(valueEnd+1);
if(nextCh ==';' || nextCh == ','){
// 去掉前面的 \ 处理这种形式的数据
while(valueEnd>0 ){
char preCh = msg.charAt(valueEnd-1);
if(preCh != '\\'){
break;
}
valueEnd--;
}
break;
}else{
valueEnd ++;
}
}else if (ch ==';' || ch == ',' || ch == '}'){
break;
}else{
valueEnd ++;
}
}while(true);
return valueEnd;
}
private String tuomin(String submsg, String key){
// idcard:身份证号, realname:姓名, bankcard:银行卡号, mobile:手机号
if("idcard".equals(key)){
return SensitiveInfoUtils.idCardNum(submsg);
}
if("realname".equals(key)){
return SensitiveInfoUtils.chineseName(submsg);
}
if("bankcard".equals(key)){
return SensitiveInfoUtils.bankCard(submsg);
}
if("mobile".equals(key)){
return SensitiveInfoUtils.mobilePhone(submsg);
}
return "";
}
}
import org.apache.commons.lang.StringUtils;
public class SensitiveInfoUtils {
/**
* [姓名] 只显示第一个汉字,其他隐藏为星号<例子:李**>
*
* @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), "*");
}
/**
* [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
*
* @param idCardNum
* @return
*/
public static String idCardNum(String idCardNum) {
if (StringUtils.isBlank(idCardNum)) {
return "";
}
String num = StringUtils.right(idCardNum, 4);
return StringUtils.leftPad(num, StringUtils.length(idCardNum), "*");
}
/**
* [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>
*
* @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), "*"), "***"));
}
/**
* [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>
*
* @param cardNum
* @return
*/
public static String bankCard(String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******"));
}
}
配置文件
代码部分完成之后,我们需要在locback.xml配置文件中增加一行配置:
<conversionRule conversionWord="msg" converterClass="com.api.filter.SensitiveDataConverter"> </conversionRule>
详细如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Logback默认配置的采用的步骤 -->
<!-- 1. 尝试在 classpath 下查找文件 logback-test.xml; -->
<!-- 2. 如果文件不存在,则查找文件 logback.xml; -->
<!-- 3. 如果两个文件都不存在,logback 用 BasicConfigurator 自动对自己进行配置,这会导致记录输出到控制台。 -->
<!-- 本机环境中只会加载该配置文件,部署服务器时请删除本文件 -->
<conversionRule conversionWord="msg" converterClass="com.api.filter.SensitiveDataConverter"> </conversionRule>
<!-- 输出控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<Pattern><![CDATA[ [%-5level] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] [%logger.%method:%line] -- %msg%n ]]></Pattern>
</encoder>
</appender>
<!-- 时间滚动输出日志 -->
<appender name="file—info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>D:/logs/aaaa.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>D:/logs/aaaa.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern><![CDATA[ [%-5level] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] [%logger.%method:%line] -- %msg%n ]]></pattern>
</encoder>
</appender>
<logger name="dfpay-auto" additivity="false">
<appender-ref ref="INFO_FILE" />
</logger>
<root level="INFO">
<appender-ref ref="console" />
</root>
</configuration>
然后就可以运行代码,输出结果:
{sign=f88898b2677e62f1ad54b9e330c0a27e, idcard=*********2762, realname=%*********************, key=c5d34d4c3c71cc45c88f32b4f13da887, mobile=132****1605, bankcard=622643******7525}
{“reason”:”成功 “,”result”:{“jobid”:”JH2131171027170837443588J6”,”realname”:”李**”,”bankcard”:”622643******7525”,”idcard”:”**************2762”,”mobile”:”132****1605”,”res”:”1”,”message”:”验证成功”},”error_code”:0}
目录:
结束语
- 代码很简单,说白了就是利用logback的Converter,自定义日志格式转换符,然后继承ClassicConverter就可以了 ,当然了还有其他的实现方式,这里就不多写了。
- 文章有不对的地方欢迎大家指正,共同进步,谢谢!!