基于logback+fastjson实现日志脱敏

一、需求背景

        日常工作中,必不可免的会将一些敏感信息,如用户名、密码、手机号、身份证号、银行账号等等打印出来,但往往为了安全,这些信息都需要进行脱敏。脱敏实际就是用一些特殊字符来替换部分值。

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);
    }
}

结果如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/965925.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RC5分组加密算法

目录 &#xff08;1&#xff09;RC5密钥扩展算法 &#xff08;2&#xff09;RC5加密算法 &#xff08;3&#xff09;RC5解密算法 RC5分组加密算法 RC5分组密码算法是1994年RSA实验室的RonaldL.Rivest教授发明的。它是参数可变的分组密码算法&#xff0c;三个可变的参数是&a…

GPU — 8 卡 GPU 服务器与 NVLink/NVSwitch 互联技术

目录 文章目录 目录8 卡 GPU 服务器GPU 互联技术分类PCIe 直连PCIe Switch 互联NVLink 互联NVLink 1.0 与 DGX-1 系统NVLink 2.0 与 DGX-1 系统NVSwitch 全互联NVSwitch 1.0 与 DGX-2 系统NVLink 3.0、NVSwitch 2.0 与 DGX A100NVLink 4.0、NVSwitch 3.0 与 DGX H100NVSwitch v…

idea——IDEA2024版本创建Sping项目无法选择Java 8

目录 一、背景二、解决方式&#xff08;替换创建项目的源地址&#xff09; 一、背景 IDEA2024创建一个springboot的项目&#xff0c;本地安装的是1.8&#xff0c;但是在使用Spring Initializr创建项目时&#xff0c;发现版本只有17、21、23。 二、解决方式&#xff08;替换创…

STM32 串口发送与接收

接线图 代码配置 根据上一章发送的代码配置&#xff0c;在GPIO配置的基础上需要再配置PA10引脚做RX接收&#xff0c;引脚模式可以选择浮空输入或者上拉输入&#xff0c;在USART配置串口模式里加上RX模式。 配置中断 //配置中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE…

储能系统-系统架构

已更新系列文章包括104、61850、modbus 、单片机等&#xff0c;欢迎关注 IEC61850实现方案和测试-1-CSDN博客 快速了解104协议-CSDN博客 104调试工具2_104协议调试工具-CSDN博客 1 电池储能系统&#xff08;BESS&#xff09; 架构 电池储能系统主要包括、电池、pcs、本地控制…

TOTP实现Google Authenticator认证工具获取6位验证码

登录遇到Google认证怎么办? TOTP是什么?(Google Authenticator) TOTP(Time-based One-Time Password)是一种基于时间的一次性密码算法,主要用于双因素身份验证。其核心原理是通过共享密钥和时间同步生成动态密码,具体步骤如下: 共享密钥:服务端与客户端预先共享一个…

清理服务器/docker容器

清理服务器 服务器或docker容器清理空间。 清理conda环境 删除不用的conda虚拟环境&#xff1a; conda env remove --name python38 conda env remove --name python310清理临时目录&#xff1a;/tmp du -sh /tmp # 查看/tmp目录的大小/tmp 目录下的文件通常是可以直接删除…

Naive UI去掉n-select下拉框边框,去掉n-input输入框边框

<template><div><div style"margin-top:10px;width: 100%;"><dade-descriptions><tr><dade-descriptions-item label"代理名称"><dade-input placeholder"代理名称"></dade-input></dade-de…

【完整版】DeepSeek-R1大模型学习笔记(架构、训练、Infra)

文章目录 0 DeepSeek系列总览1 模型架构设计基本参数专家混合模型&#xff08;MoE&#xff09;[DeepSeek-V2提出, DeepSeek-V3改良]多头潜在注意力&#xff08;MLA&#xff09;[DeepSeek-V2提出]多token预测&#xff08;MTP&#xff09;[DeepSeek-V3提出] 2 DeepSeek-R1-Zero及…

如何使用 Python 和 SQLAlchemy 结合外键映射来获取其他表中的数据

在使用 Python 和 SQLAlchemy 时&#xff0c;结合外键映射可以让你在查询时轻松地获取其他表中的数据。SQLAlchemy 提供了丰富的 ORM&#xff08;对象关系映射&#xff09;功能&#xff0c;可以让你通过定义外键关系来查询并获取关联的数据。下面我会演示如何设置外键关系&…

Python爬虫:1药城店铺爬虫(完整代码)

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…

游戏引擎学习第91天

黑板&#xff1a;澄清线性独立性 首先&#xff0c;提到线性独立时&#xff0c;之前讲解过的“最小”的概念实际上是在表达线性独立。对于二维坐标系来说&#xff0c;两个基向量是最小的&#xff0c;这两个向量是线性独立的。如果超过两个基向量&#xff0c;就会变得冗余&#…

学习率调整策略 | PyTorch 深度学习实战

前一篇文章&#xff0c;深度学习里面的而优化函数 Adam&#xff0c;SGD&#xff0c;动量法&#xff0c;AdaGrad 等 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 本篇文章内容来自于 强化学习必修课&#xff1a;引…

在 Flownex 中创建自定义工作液

在这篇博文中&#xff0c;我们将了解如何在 Flownex 中为流网添加和定义一种新的流体温度相关工作材料。 Flownex 物料管理界面 在 Flownex 中使用与温度相关的流体材料时&#xff0c;了解其特性与温度的关系非常重要。这种了解可确保准确预测各种热条件下的流体行为&#xff0…

记一次golang环境的变化

前两天编译打包了了个文件&#xff0c;把env的 goos 搞坏了 导致运行项目一直报错 先是这样 go: unsupported GOOS/GOARCH pair windows/amd64再是这样 /amd64supported GOOS/GOARCH pair linux咱就说&#xff0c;咱也是知道环境配置的有问题 &#xff08; go env GOOS &…

算法【Java】—— 动态规划之子序列问题

最长递增子序列 https://leetcode.cn/problems/longest-increasing-subsequence 状态表示&#xff1a;和之前的经验一样&#xff0c;dp[i] 表示 以 i 为结尾元素的所有递增子序列中最大长度是多少 状态转移方程推导&#xff1a;从 i 前面的元素开始寻找&#xff0c;当 nums[j…

ASP.NET Core标识框架Identity

目录 Authentication与Authorization 标识框架&#xff08;Identity&#xff09; Identity框架的使用 初始化 自定义属性 案例一&#xff1a;添加用户、角色 案例二&#xff1a;检查登录用户信息 案例三&#xff1a;实现密码的重置 步骤 Authentication与Authorizatio…

124,【8】buuctf web [极客大挑战 2019] Http

进入靶场 查看源码 点击 与url有关&#xff0c;抓包 over

windows下安装Open Web UI

windows下安装openwebui有三种方式,docker,pythonnode.js,整合包. 这里我选择的是第二种,非docker. 非Docker方式安装 1. 安装Python&#xff1a; 下载并安装Python 3.11&#xff0c;建议安装路径中不要包含中文字符&#xff0c;并勾选“Add python 3.11 to Path”选项。 安…

Mac 基于Ollama 本地部署DeepSeek离线模型

最近节日期间最火的除了《哪吒》就是deepseek了&#xff0c;毕竟又让西方各个层面都瑟瑟发抖的产品。DeepSeek凭借其强大的AI能力真的是在全球多个领域展现出强大的影响力。由于受到外部势力的恶意攻击倒是deepseek官方服务不稳定&#xff0c;国内其他厂家的适配版本也不是很稳…