关于实现log4j2日志脱敏的一种方案

情景

最近公司严格要求日志脱敏,对于敏感字段,诸如身份证号、手机号、银行卡号等用户信息进行掩码,保证日志中没有明文。

项目代码中打印日志的地方形如:

logger.info("idCard:{},phone:{},mobile:{},name:{}", idCard, phone, mobile, name);

相信很多javaer都是这么做的,现在要对日志进行掩码,怎么做?难道一行一行去改?

当然不行!

这种情况当然是…debug了!


DEBUG的重要性

第一次遇上这种问题,很多人可能手足无措,那么是时候祭出终极大招:DEBUG

从logger.info()进去,发现调用了AbstractLogger.logIfEnabled()方法,继续往下走到AbstractLogger.logMessage(),这一方法有下面两行代码:

final Message msg = messageFactory.newMessage(message, p0, p1, p2, p3);
        logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());

首先看下AbstractLogger的私有属性messageFactory是怎么来的:

public AbstractLogger() {
    this.name = getClass().getName();
    this.messageFactory = createDefaultMessageFactory();
    this.flowMessageFactory = createDefaultFlowMessageFactory();
}

public AbstractLogger(final String name) {
    this(name, createDefaultMessageFactory());
}

public AbstractLogger(final String name, final MessageFactory messageFactory) {
    this.name = name;
    this.messageFactory = messageFactory == null ? createDefaultMessageFactory() : narrow(messageFactory);
    this.flowMessageFactory = createDefaultFlowMessageFactory();
}

其三个构造方法,对于messageFactory都使用了createDefaultMessageFactory()方法

private static MessageFactory2 createDefaultMessageFactory() {
        try {
            final MessageFactory result = DEFAULT_MESSAGE_FACTORY_CLASS.newInstance();
            return narrow(result);
        } catch (final InstantiationException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

默认来自静态属性DEFAULT_MESSAGE_FACTORY_CLASS的实例:

public static final Class<? extends MessageFactory> DEFAULT_MESSAGE_FACTORY_CLASS =
            createClassForProperty("log4j2.messageFactory", ReusableMessageFactory.class,
                    ParameterizedMessageFactory.class);

仔细看看这个静态方法:

private static Class<? extends MessageFactory> createClassForProperty(final String property,
            final Class<ReusableMessageFactory> reusableParameterizedMessageFactoryClass,
            final Class<ParameterizedMessageFactory> parameterizedMessageFactoryClass) {
        try {
            final String fallback = Constants.ENABLE_THREADLOCALS ? reusableParameterizedMessageFactoryClass.getName()
                    : parameterizedMessageFactoryClass.getName();
            final String clsName = PropertiesUtil.getProperties().getStringProperty(property, fallback);
            return LoaderUtil.loadClass(clsName).asSubclass(MessageFactory.class);
        } catch (final Throwable t) {
            return parameterizedMessageFactoryClass;
        }
    }

直接从系统属性log4j2.messageFactory获取了工厂类全名(真的如此吗?)

看到这里以为要大功告成了,目前看上去是需要实现工厂类的接口,再将其配置给log4j2.messageFactory属性即可。于是就有了工厂类如下:

public class DesensitizedParameterizedMessageFactory implements MessageFactory {

    @Override
    public Message newMessage(Object message) {
        return null;
    }

    @Override
    public Message newMessage(String message) {
        return null;
    }

    @Override
    public Message newMessage(String message, Object... params) {
        return null;
    }
}

为啥非要叫XXParameterizedMessageFactory,具体细节请自行debug。

现在工厂有了,产品呢?

回到AbstractLogger.logMessage()方法,工厂类messageFactory产生的产品是Message,打开看下,(肯定)是个接口。这里我们只看由ParameterizedMessageFactory产生的ParameterizedMessage

关注其几个重要私有属性:

//消息模板,本例子中是idCard:{},phone:{},mobile:{},name:{}
private String messagePattern;
//参数数组,本例子中是数组{idCard, phone, mobile, name}
private transient Object[] argArray;
//格式化后的具体消息
private String formattedMessage;
//特殊处理异常对象
private transient Throwable throwable;
//这个很重要,是标识位{}在messagePattern中的位置
private int[] indices;
//当前indices的位置,表示使用到第几个标识位了
private int usedCount;

我们是希望在格式化的时候,就对敏感字段进行掩码,因此重点关注org.apache.logging.log4j.message.ParameterizedMessage#formatTo这个方法,debug进去后执行了org.apache.logging.log4j.message.ParameterFormatter#formatMessage2这个方法:

static void formatMessage2(final StringBuilder buffer, final String messagePattern,
            final Object[] arguments, final int argCount, final int[] indices) {
        if (messagePattern == null || arguments == null || argCount == 0) {
            buffer.append(messagePattern);
            return;
        }
        int previous = 0;
        for (int i = 0; i < argCount; i++) {
            buffer.append(messagePattern, previous, indices[i]);
            previous = indices[i] + 2;
            recursiveDeepToString(arguments[i], buffer, null);
        }
        buffer.append(messagePattern, previous, messagePattern.length());
    }

如上,我们看到在这里对arguments数组进行了循环,将其值最终append到目标StringBuilder,如果我们在这里能判断模板包含了什么敏感字符串,就可以在这里使用不同的掩码方式对字段进行处理。

怎么获取当前是哪个字读在append了呢?

前面提到的indices这个时候就可以用了,那么我们可以从模板messagePattern截取出当前要append这段字符串:

String word = messagePattern.substring(previous, indices[i]);

为了能达到掩码的目的,我们加了一个枚举类来完成对不同字段进行掩码操作:

public enum DesensitizedWords {
    idCard("idCard", 6, 3),
    phone("phone", 3, 4),
    mobile("mobile", 3, 4),
    name("name", 0, 1),
    ;
    private String word;
    private int front;
    private int tail;

    DesensitizedWords(String word, int front, int tail) {
        this.word = word;
        this.front = front;
        this.tail = tail;
    }

    public static String desensitize(String word, String val) {
        for (DesensitizedWords item : DesensitizedWords.values()) {
            if (word.contains(item.word)) {
                return hide(val, item.front, item.tail, '*');
            }
        }
        return val;
    }

    public static String hide(String src, int front, int tail, char replace) {
        if (null == src)
            return src;
        int len = src.length();
        if (front > len || tail > len) {
            return src;
        }

        StringBuilder builder = new StringBuilder();
        if (front > 0) {
            builder.append(src.substring(0, front));
        } else {
            front = 0;
        }
        String tailStr = "";
        if (tail > 0) {
            tailStr = src.substring(src.length() - tail, src.length());
        } else {
            tail = 0;
        }
        int padding = len - front - tail;
        if (padding > 0) {
            for (int i = 0; i < padding; i++) {
                builder.append(replace);
            }
        }
        builder.append(tailStr);
        return builder.toString();
    }
}

然后将org.apache.logging.log4j.message.ParameterFormatter#formatMessage2方法重写(实际上由于ParameterFormatter是final并且是package可见的,因此我们将org.apache.logging.log4j.message.ParameterizedMessage整个类copy一份为DesensitizedParameterizedMessage,并且将ParameterFormatter作为DesensitizedParameterizedMessage的内部类,并重写formatMessage2方法如下):

static void formatMessage2(final StringBuilder buffer, final String messagePattern,
                                   final Object[] arguments, final int argCount, final int[] indices) {
            if (messagePattern == null || arguments == null || argCount == 0) {
                buffer.append(messagePattern);
                return;
            }
            int previous = 0;
            for (int i = 0; i < argCount; i++) {
                String word = messagePattern.substring(previous, indices[i]);
                buffer.append(messagePattern, previous, indices[i]);
                previous = indices[i] + 2;
                StringBuilder builder = new StringBuilder();
                recursiveDeepToString(arguments[i], builder, null);
                buffer.append(DesensitizedWords.desensitize(word, builder.toString()));
            }
            buffer.append(messagePattern, previous, messagePattern.length());
        }

通过上面的改写(copy),我们已经将最基本的类创建出来,现在要考虑如何使用他们,前文有讲到使用系统属性log4j2.messageFactory来配置工厂类,实际上并没有起作用,再次debug后发现,我们需要在resources下加一个log4j2.component.properties文件来配置这个属性才能生效,文件配置如下:

log4j2.messageFactory=com.X.Xx.DesensitizedParameterizedMessageFactory

记得将工厂类产生的产品改为DesensitizedParameterizedMessage哟。


效果

测试用例

String idCard = "210002197812129527";
String name = "周星星";
String phone = "14700000000";
String mobile = "14700000000";
logger.info("明文数据:{},{},{},{}", idCard, phone, mobile, name);
logger.info("idCard:{},phone:{},mobile:{},name:{}", idCard, phone, mobile, name);
logger.error("error-idCard:{},error-phone:{},error-mobile:{},error-name:{}", idCard, phone, mobile, name);
logger.warn("warn-idCard:{},warn-phone:{},warn-mobile:{},warn-name:{}", idCard, phone, mobile, name);
logger.debug("debug-idCard:{},debug-phone:{},debug-mobile:{},debug-name:{}", idCard, phone, mobile, name);

效果如下

2017-12-06 10:48:39.490 [http-bio-8080-exec-1:563179] [DemoController.java:83] - [INFO] 明文数据:210002188012120359,14700000000,14700000000,周星星
2017-12-06 10:48:39.491 [http-bio-8080-exec-1:563180] [DemoController.java:84] - [INFO] idCard:210002*********359,phone:147****0000,mobile:147****0000,name:**星
2017-12-06 10:48:39.491 [http-bio-8080-exec-1:563180] [DemoController.java:85] - [ERROR] error-idCard:210002*********359,error-phone:147****0000,error-mobile:147****0000,error-name:**星
2017-12-06 10:48:39.492 [http-bio-8080-exec-1:563181] [DemoController.java:86] - [WARN] warn-idCard:210002*********359,warn-phone:147****0000,warn-mobile:147****0000,warn-name:**星
2017-12-06 10:48:39.493 [http-bio-8080-exec-1:563182] [DemoController.java:87] - [DEBUG] debug-idCard:210002*********359,debug-phone:147****0000,debug-mobile:147****0000,debug-name:**星

源码

终于到了大家喜欢的环节(没有做裁剪,自行操作)。

DesensitizedParameterizedMessage.java

import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.StringBuilderFormattable;

import java.text.SimpleDateFormat;
import java.util.*;

public class DesensitizedParameterizedMessage implements Message, StringBuilderFormattable {
    // Should this be configurable?
    private static final int DEFAULT_STRING_BUILDER_SIZE = 255;

    /**
     * Prefix for recursion.
     */
    public static final String RECURSION_PREFIX = ParameterFormatter.RECURSION_PREFIX;
    /**
     * Suffix for recursion.
     */
    public static final String RECURSION_SUFFIX = ParameterFormatter.RECURSION_SUFFIX;

    /**
     * Prefix for errors.
     */
    public static final String ERROR_PREFIX = ParameterFormatter.ERROR_PREFIX;

    /**
     * Separator for errors.
     */
    public static final String ERROR_SEPARATOR = ParameterFormatter.ERROR_SEPARATOR;

    /**
     * Separator for error messages.
     */
    public static final String ERROR_MSG_SEPARATOR = ParameterFormatter.ERROR_MSG_SEPARATOR;

    /**
     * Suffix for errors.
     */
    public static final String ERROR_SUFFIX = ParameterFormatter.ERROR_SUFFIX;

    private static final long serialVersionUID = -665975803997290697L;

    private static final int HASHVAL = 31;

    // storing JDK classes in ThreadLocals does not cause memory leaks in web apps, so this is okay
    private static ThreadLocal<StringBuilder> threadLocalStringBuilder = new ThreadLocal<>();

    private String messagePattern;
    private transient Object[] argArray;

    private String formattedMessage;
    private transient Throwable throwable;
    private int[] indices;
    private int usedCount;

    public DesensitizedParameterizedMessage(final String messagePattern, final Object[] arguments, final Throwable throwable) {
        this.argArray = arguments;
        this.throwable = throwable;
        init(messagePattern);
    }

    public DesensitizedParameterizedMessage(final String messagePattern, final Object... arguments) {
        this.argArray = arguments;
        init(messagePattern);
    }

    /**
     * Constructor with a pattern and a single parameter.
     *
     * @param messagePattern The message pattern.
     * @param arg            The parameter.
     */
    public DesensitizedParameterizedMessage(final String messagePattern, final Object arg) {
        this(messagePattern, new Object[]{arg});
    }

    public DesensitizedParameterizedMessage(final String messagePattern, final Object arg0, final Object arg1) {
        this(messagePattern, new Object[]{arg0, arg1});
    }

    private void init(final String messagePattern) {
        this.messagePattern = messagePattern;
        final int len = Math.max(1, messagePattern == null ? 0 : messagePattern.length() >> 1); // divide by 2
        this.indices = new int[len]; // LOG4J2-1542 ensure non-zero array length
        final int placeholders = ParameterFormatter.countArgumentPlaceholders2(messagePattern, indices);
        initThrowable(argArray, placeholders);
        this.usedCount = Math.min(placeholders, argArray == null ? 0 : argArray.length);
    }

    private void initThrowable(final Object[] params, final int usedParams) {
        if (params != null) {
            final int argCount = params.length;
            if (usedParams < argCount && this.throwable == null && params[argCount - 1] instanceof Throwable) {
                this.throwable = (Throwable) params[argCount - 1];
            }
        }
    }

    @Override
    public String getFormat() {
        return messagePattern;
    }

    @Override
    public Object[] getParameters() {
        return argArray;
    }

    @Override
    public Throwable getThrowable() {
        return throwable;
    }

    @Override
    public String getFormattedMessage() {
        if (formattedMessage == null) {
            final StringBuilder buffer = getThreadLocalStringBuilder();
            formatTo(buffer);
            formattedMessage = buffer.toString();
        }
        return formattedMessage;
    }

    private static StringBuilder getThreadLocalStringBuilder() {
        StringBuilder buffer = threadLocalStringBuilder.get();
        if (buffer == null) {
            buffer = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
            threadLocalStringBuilder.set(buffer);
        }
        buffer.setLength(0);
        return buffer;
    }

    @Override
    public void formatTo(final StringBuilder buffer) {
        if (formattedMessage != null) {
            buffer.append(formattedMessage);
        } else {
            if (indices[0] < 0) {
                ParameterFormatter.formatMessage(buffer, messagePattern, argArray, usedCount);
            } else {
                ParameterFormatter.formatMessage2(buffer, messagePattern, argArray, usedCount, indices);
            }
        }
    }

    public static String format(final String messagePattern, final Object[] arguments) {
        return ParameterFormatter.format(messagePattern, arguments);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final DesensitizedParameterizedMessage that = (DesensitizedParameterizedMessage) o;

        if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) {
            return false;
        }
        if (!Arrays.equals(this.argArray, this.argArray)) {
            return false;
        }
        //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = messagePattern != null ? messagePattern.hashCode() : 0;
        result = HASHVAL * result + (argArray != null ? Arrays.hashCode(argArray) : 0);
        return result;
    }

    public static int countArgumentPlaceholders(final String messagePattern) {
        return ParameterFormatter.countArgumentPlaceholders(messagePattern);
    }

    public static String deepToString(final Object o) {
        return ParameterFormatter.deepToString(o);
    }

    public static String identityToString(final Object obj) {
        return ParameterFormatter.identityToString(obj);
    }

    @Override
    public String toString() {
        return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
                Arrays.toString(argArray) + ", throwable=" + throwable + ']';
    }


    static class ParameterFormatter {
        /**
         * Prefix for recursion.
         */
        static final String RECURSION_PREFIX = "[...";
        /**
         * Suffix for recursion.
         */
        static final String RECURSION_SUFFIX = "...]";

        /**
         * Prefix for errors.
         */
        static final String ERROR_PREFIX = "[!!!";
        /**
         * Separator for errors.
         */
        static final String ERROR_SEPARATOR = "=>";
        /**
         * Separator for error messages.
         */
        static final String ERROR_MSG_SEPARATOR = ":";
        /**
         * Suffix for errors.
         */
        static final String ERROR_SUFFIX = "!!!]";

        private static final char DELIM_START = '{';
        private static final char DELIM_STOP = '}';
        private static final char ESCAPE_CHAR = '\\';

        private static ThreadLocal<SimpleDateFormat> threadLocalSimpleDateFormat = new ThreadLocal<>();

        private ParameterFormatter() {
        }

        static int countArgumentPlaceholders(final String messagePattern) {
            if (messagePattern == null) {
                return 0;
            }
            final int length = messagePattern.length();
            int result = 0;
            boolean isEscaped = false;
            for (int i = 0; i < length - 1; i++) {
                final char curChar = messagePattern.charAt(i);
                if (curChar == ESCAPE_CHAR) {
                    isEscaped = !isEscaped;
                } else if (curChar == DELIM_START) {
                    if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
                        result++;
                        i++;
                    }
                    isEscaped = false;
                } else {
                    isEscaped = false;
                }
            }
            return result;
        }

        static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
            if (messagePattern == null) {
                return 0;
            }
            final int length = messagePattern.length();
            int result = 0;
            boolean isEscaped = false;
            for (int i = 0; i < length - 1; i++) {
                final char curChar = messagePattern.charAt(i);
                if (curChar == ESCAPE_CHAR) {
                    isEscaped = !isEscaped;
                    indices[0] = -1; // escaping means fast path is not available...
                    result++;
                } else if (curChar == DELIM_START) {
                    if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
                        indices[result] = i;
                        result++;
                        i++;
                    }
                    isEscaped = false;
                } else {
                    isEscaped = false;
                }
            }
            return result;
        }

        static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
            int result = 0;
            boolean isEscaped = false;
            for (int i = 0; i < length - 1; i++) {
                final char curChar = messagePattern[i];
                if (curChar == ESCAPE_CHAR) {
                    isEscaped = !isEscaped;
                } else if (curChar == DELIM_START) {
                    if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
                        indices[result] = i;
                        result++;
                        i++;
                    }
                    isEscaped = false;
                } else {
                    isEscaped = false;
                }
            }
            return result;
        }


        static String format(final String messagePattern, final Object[] arguments) {
            final StringBuilder result = new StringBuilder();
            final int argCount = arguments == null ? 0 : arguments.length;
            formatMessage(result, messagePattern, arguments, argCount);
            return result.toString();
        }


        static void formatMessage2(final StringBuilder buffer, final String messagePattern,
                                   final Object[] arguments, final int argCount, final int[] indices) {
            if (messagePattern == null || arguments == null || argCount == 0) {
                buffer.append(messagePattern);
                return;
            }
            int previous = 0;
            for (int i = 0; i < argCount; i++) {
                String word = messagePattern.substring(previous, indices[i]);
                buffer.append(messagePattern, previous, indices[i]);
                previous = indices[i] + 2;
                StringBuilder builder = new StringBuilder();
                recursiveDeepToString(arguments[i], builder, null);
                buffer.append(DesensitizedWords.desensitize(word, builder.toString()));
            }
            buffer.append(messagePattern, previous, messagePattern.length());
        }

        static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
                                   final Object[] arguments, final int argCount, final int[] indices) {
            if (messagePattern == null) {
                return;
            }
            if (arguments == null || argCount == 0) {
                buffer.append(messagePattern);
                return;
            }
            int previous = 0;
            for (int i = 0; i < argCount; i++) {
                buffer.append(messagePattern, previous, indices[i]);
                previous = indices[i] + 2;
                recursiveDeepToString(arguments[i], buffer, null);
            }
            buffer.append(messagePattern, previous, patternLength);
        }

        static void formatMessage(final StringBuilder buffer, final String messagePattern,
                                  final Object[] arguments, final int argCount) {
            if (messagePattern == null || arguments == null || argCount == 0) {
                buffer.append(messagePattern);
                return;
            }
            int escapeCounter = 0;
            int currentArgument = 0;
            int i = 0;
            final int len = messagePattern.length();
            for (; i < len - 1; i++) { // last char is excluded from the loop
                final char curChar = messagePattern.charAt(i);
                if (curChar == ESCAPE_CHAR) {
                    escapeCounter++;
                } else {
                    if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
                        i++;

                        // write escaped escape chars
                        writeEscapedEscapeChars(escapeCounter, buffer);

                        if (isOdd(escapeCounter)) {
                            // i.e. escaped: write escaped escape chars
                            writeDelimPair(buffer);
                        } else {
                            // unescaped
                            writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
                            currentArgument++;
                        }
                    } else {
                        handleLiteralChar(buffer, escapeCounter, curChar);
                    }
                    escapeCounter = 0;
                }
            }
            handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
        }

        private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
            return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
        }


        private static void handleRemainingCharIfAny(final String messagePattern, final int len,
                                                     final StringBuilder buffer, final int escapeCounter, final int i) {
            if (i == len - 1) {
                final char curChar = messagePattern.charAt(i);
                handleLastChar(buffer, escapeCounter, curChar);
            }
        }

        private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
            if (curChar == ESCAPE_CHAR) {
                writeUnescapedEscapeChars(escapeCounter + 1, buffer);
            } else {
                handleLiteralChar(buffer, escapeCounter, curChar);
            }
        }


        private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {

            writeUnescapedEscapeChars(escapeCounter, buffer);
            buffer.append(curChar);
        }

        private static void writeDelimPair(final StringBuilder buffer) {
            buffer.append(DELIM_START);
            buffer.append(DELIM_STOP);
        }

        private static boolean isOdd(final int number) {
            return (number & 1) == 1;
        }


        private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
            final int escapedEscapes = escapeCounter >> 1; // divide by two
            writeUnescapedEscapeChars(escapedEscapes, buffer);
        }


        private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
            while (escapeCounter > 0) {
                buffer.append(ESCAPE_CHAR);
                escapeCounter--;
            }
        }

        private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
                                                final StringBuilder buffer) {
            if (currentArgument < argCount) {
                recursiveDeepToString(arguments[currentArgument], buffer, null);
            } else {
                writeDelimPair(buffer);
            }
        }

        static String deepToString(final Object o) {
            if (o == null) {
                return null;
            }
            if (o instanceof String) {
                return (String) o;
            }
            final StringBuilder str = new StringBuilder();
            final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;)
            recursiveDeepToString(o, str, dejaVu);
            return str.toString();
        }

        private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
            if (appendSpecialTypes(o, str)) {
                return;
            }
            if (isMaybeRecursive(o)) {
                appendPotentiallyRecursiveValue(o, str, dejaVu);
            } else {
                tryObjectToString(o, str);
            }
        }

        private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
            if (o == null || o instanceof String) {
                str.append((String) o);
                return true;
            } else if (o instanceof CharSequence) {
                str.append((CharSequence) o);
                return true;
            } else if (o instanceof StringBuilderFormattable) {
                ((StringBuilderFormattable) o).formatTo(str);
                return true;
            } else if (o instanceof Integer) { // LOG4J2-1415 unbox auto-boxed primitives to avoid calling toString()
                str.append(((Integer) o).intValue());
                return true;
            } else if (o instanceof Long) {
                str.append(((Long) o).longValue());
                return true;
            } else if (o instanceof Double) {
                str.append(((Double) o).doubleValue());
                return true;
            } else if (o instanceof Boolean) {
                str.append(((Boolean) o).booleanValue());
                return true;
            } else if (o instanceof Character) {
                str.append(((Character) o).charValue());
                return true;
            } else if (o instanceof Short) {
                str.append(((Short) o).shortValue());
                return true;
            } else if (o instanceof Float) {
                str.append(((Float) o).floatValue());
                return true;
            }
            return appendDate(o, str);
        }

        private static boolean appendDate(final Object o, final StringBuilder str) {
            if (!(o instanceof Date)) {
                return false;
            }
            final Date date = (Date) o;
            final SimpleDateFormat format = getSimpleDateFormat();
            str.append(format.format(date));
            return true;
        }

        private static SimpleDateFormat getSimpleDateFormat() {
            SimpleDateFormat result = threadLocalSimpleDateFormat.get();
            if (result == null) {
                result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
                threadLocalSimpleDateFormat.set(result);
            }
            return result;
        }

        private static boolean isMaybeRecursive(final Object o) {
            return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
        }

        private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
                                                            final Set<String> dejaVu) {
            final Class<?> oClass = o.getClass();
            if (oClass.isArray()) {
                appendArray(o, str, dejaVu, oClass);
            } else if (o instanceof Map) {
                appendMap(o, str, dejaVu);
            } else if (o instanceof Collection) {
                appendCollection(o, str, dejaVu);
            }
        }

        private static void appendArray(final Object o, final StringBuilder str, Set<String> dejaVu,
                                        final Class<?> oClass) {
            if (oClass == byte[].class) {
                str.append(Arrays.toString((byte[]) o));
            } else if (oClass == short[].class) {
                str.append(Arrays.toString((short[]) o));
            } else if (oClass == int[].class) {
                str.append(Arrays.toString((int[]) o));
            } else if (oClass == long[].class) {
                str.append(Arrays.toString((long[]) o));
            } else if (oClass == float[].class) {
                str.append(Arrays.toString((float[]) o));
            } else if (oClass == double[].class) {
                str.append(Arrays.toString((double[]) o));
            } else if (oClass == boolean[].class) {
                str.append(Arrays.toString((boolean[]) o));
            } else if (oClass == char[].class) {
                str.append(Arrays.toString((char[]) o));
            } else {
                if (dejaVu == null) {
                    dejaVu = new HashSet<>();
                }
                // special handling of container Object[]
                final String id = identityToString(o);
                if (dejaVu.contains(id)) {
                    str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
                } else {
                    dejaVu.add(id);
                    final Object[] oArray = (Object[]) o;
                    str.append('[');
                    boolean first = true;
                    for (final Object current : oArray) {
                        if (first) {
                            first = false;
                        } else {
                            str.append(", ");
                        }
                        recursiveDeepToString(current, str, new HashSet<>(dejaVu));
                    }
                    str.append(']');
                }
                //str.append(Arrays.deepToString((Object[]) o));
            }
        }

        private static void appendMap(final Object o, final StringBuilder str, Set<String> dejaVu) {
            // special handling of container Map
            if (dejaVu == null) {
                dejaVu = new HashSet<>();
            }
            final String id = identityToString(o);
            if (dejaVu.contains(id)) {
                str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
            } else {
                dejaVu.add(id);
                final Map<?, ?> oMap = (Map<?, ?>) o;
                str.append('{');
                boolean isFirst = true;
                for (final Object o1 : oMap.entrySet()) {
                    final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
                    if (isFirst) {
                        isFirst = false;
                    } else {
                        str.append(", ");
                    }
                    final Object key = current.getKey();
                    final Object value = current.getValue();
                    recursiveDeepToString(key, str, new HashSet<>(dejaVu));
                    str.append('=');
                    recursiveDeepToString(value, str, new HashSet<>(dejaVu));
                }
                str.append('}');
            }
        }

        private static void appendCollection(final Object o, final StringBuilder str, Set<String> dejaVu) {
            // special handling of container Collection
            if (dejaVu == null) {
                dejaVu = new HashSet<>();
            }
            final String id = identityToString(o);
            if (dejaVu.contains(id)) {
                str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
            } else {
                dejaVu.add(id);
                final Collection<?> oCol = (Collection<?>) o;
                str.append('[');
                boolean isFirst = true;
                for (final Object anOCol : oCol) {
                    if (isFirst) {
                        isFirst = false;
                    } else {
                        str.append(", ");
                    }
                    recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
                }
                str.append(']');
            }
        }

        private static void tryObjectToString(final Object o, final StringBuilder str) {
            // it's just some other Object, we can only use toString().
            try {
                str.append(o.toString());
            } catch (final Throwable t) {
                handleErrorInObjectToString(o, str, t);
            }
        }

        private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
            str.append(ERROR_PREFIX);
            str.append(identityToString(o));
            str.append(ERROR_SEPARATOR);
            final String msg = t.getMessage();
            final String className = t.getClass().getName();
            str.append(className);
            if (!className.equals(msg)) {
                str.append(ERROR_MSG_SEPARATOR);
                str.append(msg);
            }
            str.append(ERROR_SUFFIX);
        }

        static String identityToString(final Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
        }

    }
}

DesensitizedParameterizedMessageFactory.java

import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
public class DesensitizedParameterizedMessageFactory implements MessageFactory {

    private ParameterizedMessageFactory messageFactory = ParameterizedMessageFactory.INSTANCE;

    @Override
    public Message newMessage(Object message) {
        return messageFactory.newMessage(message);
    }

    @Override
    public Message newMessage(String message) {
        return new DesensitizedParameterizedMessage(message);
    }

    @Override
    public Message newMessage(String message, Object... params) {
        return new DesensitizedParameterizedMessage(message, params);
    }
}

DesensitizedWords.java见上文


后记

这次改动全部是在debug中完成的(太懒,没有截图),没有列举所有情况(谁知道你们打日志的姿势长啥样呢),可能有些坑在里面。

后来看了下官方文档,有说到一些扩展点(https://logging.apache.org/log4j/2.x/manual/extending.html),上面的改动可以看成是MessageFactory(https://logging.apache.org/log4j/2.x/manual/extending.html#MessageFactory)的扩展,目前我们使用ParameterizedMessageFactory还没有什么问题,其他的要各位看官自行操作了。

(第一次写技术文章,文笔不咋地,后面会慢慢改进的(:哈。)

  • 5
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,以下是一个简单的log4j2日志脱敏的过滤器示例: 首先,在log4j2配置文件中添加以下内容: ```xml <Configuration> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <filters> <Filter type="Script" language="groovy" script="if (logEvent.getMessage().getFormattedMessage().contains('password')) {return Result.DENY;} else {return Result.NEUTRAL;}"/> </filters> </Console> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration> ``` 其中,filters标签用于添加过滤器,type属性为“Script”,表示使用脚本过滤器,language属性为“groovy”,表示使用Groovy语言编写脚本。 然后,在script属性中添加以下Groovy脚本: ```groovy if (logEvent.getMessage().getFormattedMessage().contains('password')) { def message = logEvent.getMessage().getFormattedMessage().replaceAll(/password=\S+/, 'password=******') return new LogEvent(loggerName, logEvent.getMarker(), loggerFqcn, logEvent.getLevel(), message, logEvent.getThrown(), logEvent.getContextMap(), logEvent.getContextStack(), logEvent.getThreadName(), logEvent.getSource(), logEvent.getTimeMillis()) } else { return logEvent } ``` 该脚本会检查日志消息中是否包含“password”关键字,如果包含,则将“password”后面的字符替换为“******”,然后返回修改后的LogEvent对象。如果不包含,则直接返回原始的LogEvent对象。 这样配置后,当日志中包含“password”关键字时,日志消息中的“password”后面的字符就会被替换为“******”,从而实现日志脱敏的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值