我需要能够搜索事件中的任何一个模式,并将模式中的文本替换为遮罩值。这是我们应用程序中的一个功能,用于防止敏感信息落入日志。由于信息可能来自各种各样的来源,因此对所有输入应用过滤器是不实际的。此外,toString()除了日志记录之外还有其他用途,我不希望toString()为所有调用统一屏蔽(仅日志记录)。
我已经尝试在logback.xml中使用%replace方法:
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'f k="pin">(.*?)</f','f k="pin">**********</f'}%n</pattern>
这是成功的(在用字符实体替换尖括号之后),但它只能替换单个模式。我还想执行
的等价物<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'pin=(.*?),','pin=**********,'}%n</pattern>
,但不能。在一个%的替换中没有办法掩盖两个模式。
在interblags上被广泛讨论的另一种方式是扩展appender/encoder/layout层次结构,但是每次尝试拦截ilogingevent都会导致整个系统崩溃,通常是通过实例化错误或UnsupportedOperationException。
例如,我尝试扩展PatternLayout:
@Component("maskingPatternLayout")
public class MaskingPatternLayout extends PatternLayout {
@Autowired
private Environment env;
@Override
public String doLayout(ILoggingEvent event) {
String message=super.doLayout(event);
String patternsProperty = env.getProperty("bowdleriser.patterns");
if( patternsProperty != null ) {
String[] patterns = patternsProperty.split("|");
for (int i = 0; i < patterns.length; i++ ) {
Pattern pattern = Pattern.compile(patterns[i]);
Matcher matcher = pattern.matcher(event.getMessage());
matcher.replaceAll("*");
}
} else {
System.out.println("Bowdleriser not cleaning! Naughty strings are getting through!");
}
return message;
}
}
然后调整logback.xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<layout class="com.touchcorp.touchpoint.utils.MaskingPatternLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/touchpoint.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>logs/touchpoint.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>
<encoder>
<layout class="com.touchcorp.touchpoint.utils.MaskingPatternLayout">
<pattern>%date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</layout>
</encoder>
</appender>
<logger name="com.touchcorp.touchpoint" level="DEBUG" />
<logger name="org.springframework.web.servlet.mvc" level="TRACE" />
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
</root>
</configuration>
我已经尝试了许多其他插入,所以我想知道是否有人已经真正实现了我正在尝试,如果他们可以提供任何线索或解决方案。
您需要使用LayoutWrappingEncoder
来包装布局。而且我相信你不能在这里使用spring,因为logback不是由spring管理的。
这是更新后的类。
public class MaskingPatternLayout extends PatternLayout {
private String patternsProperty;
public String getPatternsProperty() {
return patternsProperty;
}
public void setPatternsProperty(String patternsProperty) {
this.patternsProperty = patternsProperty;
}
@Override
public String doLayout(ILoggingEvent event) {
String message = super.doLayout(event);
if (patternsProperty != null) {
String[] patterns = patternsProperty.split("\|");
for (int i = 0; i < patterns.length; i++) {
Pattern pattern = Pattern.compile(patterns[i]);
Matcher matcher = pattern.matcher(event.getMessage());
if (matcher.find()) {
message = matcher.replaceAll("*");
}
}
} else {
}
return message;
}
}
和样例logback.xml
<appender name="fileAppender1" class="ch.qos.logback.core.FileAppender">
<file>c:/logs/kp-ws.log</file>
<append>true</append>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.kp.MaskingPatternLayout">
<patternsProperty>.*password.*|.*karthik.*</patternsProperty>
<pattern>%d [%thread] %-5level %logger{35} - %msg%n</pattern>
</layout>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="fileAppender1" />
</root>
更新这里是更好的方法,在init本身中设置Pattern。这样我们就可以避免一次又一次地重新创建Pattern,并且这个实现更接近实际的用例。
public class MaskingPatternLayout extends PatternLayout {
private String patternsProperty;
private Optional<Pattern> pattern;
public String getPatternsProperty() {
return patternsProperty;
}
public void setPatternsProperty(String patternsProperty) {
this.patternsProperty = patternsProperty;
if (this.patternsProperty != null) {
this.pattern = Optional.of(Pattern.compile(patternsProperty, Pattern.MULTILINE));
} else {
this.pattern = Optional.empty();
}
}
@Override
public String doLayout(ILoggingEvent event) {
final StringBuilder message = new StringBuilder(super.doLayout(event));
if (pattern.isPresent()) {
Matcher matcher = pattern.get().matcher(message);
while (matcher.find()) {
int group = 1;
while (group <= matcher.groupCount()) {
if (matcher.group(group) != null) {
for (int i = matcher.start(group); i < matcher.end(group); i++) {
message.setCharAt(i, '*');
}
}
group++;
}
}
}
return message.toString();
}
}
和更新后的配置文件。
<appender name="fileAppender1" class="ch.qos.logback.core.FileAppender">
<file>c:/logs/kp-ws.log</file>
<append>true</append>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.kp.MaskingPatternLayout">
<patternsProperty>(password)|(karthik)</patternsProperty>
<pattern>%d [%thread] %-5level %logger{35} - %msg%n</pattern>
</layout>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="fileAppender1" />
</root>
输出My username=test and password=*******
来自文档:
replace(p){r, t}
模式p
可以任意复杂,特别是可以包含多个转换关键字。
面对同样的问题,必须在消息中替换2种模式,我只是试图chain
,所以p
只是replace
的调用,在我的情况下:
%replace( %replace(%msg){'regex1', 'replacement1'} ){'regex2', 'replacement2'}
工作得很好,虽然我想知道我是否有点推动它,p
确实可以任意复杂。
围绕自定义CompositeConverter和在日志back中定义引用自定义转换器的<conversionRule ...>
,有一种非常相似但略有不同的方法。
在我的一个技术演示项目中,我定义了一个MaskingConverter类,它定义了一系列模式,日志事件是在匹配更新的情况下进行分析的,这些模式在我的logback配置中使用。
因为只有链接的答案在这里不那么受欢迎,所以我将在这里发布代码的重要部分,并解释它的作用以及为什么它是这样设置的。从基于java的自定义转换器类开始:public class MaskingConverter<E extends ILoggingEvent> extends CompositeConverter<E> {
public static final String CONFIDENTIAL = "CONFIDENTIAL";
public static final Marker CONFIDENTIAL_MARKER = MarkerFactory.getMarker(CONFIDENTIAL);
private Pattern keyValPattern;
private Pattern basicAuthPattern;
private Pattern urlAuthorizationPattern;
@Override
public void start() {
keyValPattern = Pattern.compile("(pw|pwd|password)=.*?(&|$)");
basicAuthPattern = Pattern.compile("(B|b)asic ([a-zA-Z0-9+/=]{3})[a-zA-Z0-9+/=]*([a-zA-Z0-9+/=]{3})");
urlAuthorizationPattern = Pattern.compile("//(.*?):.*?@");
super.start();
}
@Override
protected String transform(E event, String in) {
if (!started) {
return in;
}
Marker marker = event.getMarker();
if (null != marker && CONFIDENTIAL.equals(marker.getName())) {
// key=value[&...] matching
Matcher keyValMatcher = keyValPattern.matcher(in);
// Authorization: Basic dXNlcjpwYXNzd29yZA==
Matcher basicAuthMatcher = basicAuthPattern.matcher(in);
// sftp://user:password@host:port/path/to/resource
Matcher urlAuthMatcher = urlAuthorizationPattern.matcher(in);
if (keyValMatcher.find()) {
String replacement = "$1=XXX$2";
return keyValMatcher.replaceAll(replacement);
} else if (basicAuthMatcher.find()) {
return basicAuthMatcher.replaceAll("$1asic $2XXX$3");
} else if (urlAuthMatcher.find()) {
return urlAuthMatcher.replaceAll("//$1:XXX@");
}
}
return in;
}
}
这个类定义了许多RegEx模式,应该与相应的日志行进行比较,并在匹配时通过屏蔽密码导致事件更新。
注意,此代码示例假设日志行只包含一种密码。当然,如果您想要探测每一行中的多个模式匹配,您可以根据需要自由调整行为。
要应用此转换器,只需在logback配置中添加以下行:
<conversionRule conversionWord="mask" converterClass="at.rovo.awsxray.utils.MaskingConverter"/>
定义了一个新函数mask
,该函数可以在模式中使用,以掩盖与自定义转换器中定义的任何模式匹配的任何日志事件。这个函数现在可以在模式中使用,告诉Logback对每个日志事件执行逻辑。各自的模式可能是如下所示:
<property name="patternValue"
value="%date{yyyy-MM-dd HH:mm:ss} [%-5level] - %X{FILE_ID} - %mask(%msg) [%thread] [%logger{5}] %n"/>
<!-- Appender definitions-->
<appender class="ch.qos.logback.core.ConsoleAppender" name="console">
<encoder>
<pattern>${patternValue}</pattern>
</encoder>
</appender>
,其中%mask(%msg)
将把原始日志行作为输入,并对传递给该函数的每一行执行密码屏蔽。
由于探测每行中的一个或多个模式匹配可能代价高昂,因此上面的Java代码包括可以在日志语句中使用的标记,以便将日志语句本身的某些元信息发送给Logback/SLF4J。基于这些标记,可以实现不同的行为。在所提供的场景中,可以使用标记接口告诉Logback,相应的日志行包含机密信息,因此如果匹配则需要屏蔽。任何未标记为机密的日志行将被此转换器忽略,这有助于更快地抽取行,因为不需要在这些行上执行模式匹配。
在Java中这样的标记可以像这样添加到日志语句中:
LOG.debug(MaskingConverter.CONFIDENTIAL_MARKER, "Received basic auth header: {}",
connection.getBasicAuthentication());
,对于上面提到的自定义转换器,它可能产生类似于Received basic auth header: Basic QlRXXXlQ=
的日志行,它保留了第一个和最后两个字符,但用XXX
混淆了中间的位。
这是我的方法,也许它可以帮助别人
试试这个。1. 首先,我们应该创建一个类来处理日志(每一行)
public class PatternMaskingLayout extends PatternLayout {
private Pattern multilinePattern;
private List<String> maskPatterns = new ArrayList<>();
public void addMaskPattern(String maskPattern) { // invoked for every single entry in the xml
maskPatterns.add(maskPattern);
multilinePattern = Pattern.compile(
String.join("|", maskPatterns), // build pattern using logical OR
Pattern.MULTILINE
);
}
@Override
public String doLayout(ILoggingEvent event) {
return maskMessage(super.doLayout(event)); // calling superclass method is required
}
private String maskMessage(String message) {
if (multilinePattern == null) {
return message;
}
StringBuilder sb = new StringBuilder(message);
Matcher matcher = multilinePattern.matcher(sb);
while (matcher.find()) {
if (matcher.group().contains("creditCard")) {
maskCreditCard(sb, matcher);
} else if (matcher.group().contains("email")) {
// your logic for this case
}
}
return sb.toString();
}
private void maskCreditCard(StringBuilder sb, Matcher matcher) {
//here is our main logic for masking sensitive data
String targetExpression = matcher.group();
String[] split = targetExpression.split("=");
String pan = split[1];
String maskedPan = Utils.getMaskedPan(pan);
int start = matcher.start() + split[0].length() + 1;
int end = matcher.end();
sb.replace(start, end, maskedPan);
}
}
第二步是我们应该在logback.xml
中为logback创建appender<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="com.bpcbt.micro.utils.PatternMaskingLayout"> <maskPattern>creditCard=d+</maskPattern> <!-- SourcePan pattern --> <pattern>%d{dd/MM/yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%ex</pattern>--> </layout> </encoder>
现在我们可以在代码中使用logger
log.info("card context set for creditCard={}", creditCard);
因此,我们将看到
从日志中取出一行
信用卡上下文设置为creditCard=11111******111
如果没有这些选项,我们的日志将是这样的
card context set for creditCard=1111111111111
我使用了基于图书馆https://github.com/tersesystems/terse-logback的RegexCensor的审查器。在logback.xml
<!--censoring information-->
<newRule pattern="*/censor" actionClass="com.tersesystems.logback.censor.CensorAction"/>
<conversionRule conversionWord="censor" converterClass="com.tersesystems.logback.censor.CensorConverter" />
<!--impl inspired by com.tersesystems.logback.censor.RegexCensor -->
<censor name="censor-sensitive" class="com.mycompaqny.config.logging.SensitiveDataCensor"></censor>
,我把列表正则表达式替换。
@Getter@Setter
public class SensitiveDataCensor extends ContextAwareBase implements Censor, LifeCycle {
protected volatile boolean started = false;
protected String name;
private List<Pair<Pattern, String>> replacementPhrases = new ArrayList<>();
public void start() {
String ssnJsonPattern = ""(ssn|socialSecurityNumber)("\W*:\W*".*?)-(.*?)"";
replacementPhrases.add(Pair.of(Pattern.compile(ssnJsonPattern), ""$1$2-****""));
String ssnXmlPattern = "<(ssn|socialSecurityNumber)>(\W*.*?)-(.*?)</";
replacementPhrases.add(Pair.of(Pattern.compile(ssnXmlPattern), "<$1>$2-****</"));
started = true;
}
public void stop() {
replacementPhrases.clear();
started = false;
}
public CharSequence censorText(CharSequence original) {
CharSequence outcome = original;
for (Pair<Pattern, String> replacementPhrase : replacementPhrases) {
outcome = replacementPhrase.getLeft().matcher(outcome).replaceAll(replacementPhrase.getRight());
}
return outcome;
}
}
并在logback.xml中使用,如下所示
<message>[ignore]</message> <---- IMPORTANT to disable original message field so you get only censored message
...
<pattern>
{"message": "%censor(%msg){censor-sensitive}"}
</pattern>
我试图在我的演示项目日志中隐藏一些敏感数据。我尝试过,但它没有为我工作,因为Java反射,因为我把变量名作为模式。我正在添加对我有效的解决方案,以防它也有助于其他人。
我在logback.xml(内部编码器标签)文件中添加了以下代码,用于屏蔽日志中的field1和field2信息。
<encoder class="com.demo.config.CustomJsonMaskLogEncoder">
<patterns>
<pattern>"field1"s*:s*"(.*?)"</pattern>
<pattern>"field2"s*:s*"(.*?)"</pattern>
<pattern>%-5p [%d{ISO8601,UTC}] [%thread] %c: %m%n%rootException</pattern>
</patterns>
</encoder>
我写了一个CustomJsonMaskLogEncoder,它按照正则表达式屏蔽字段数据。
package com.demo.config;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import java.util.ArrayList;
import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder;
import org.slf4j.LoggerFactory;
public class CustomJsonMaskLogEncoder extends LoggingEventCompositeJsonEncoder {
private final CustomPatternMaskingLayout customPatternMaskingLayout;
private boolean maskEnabled;
public JsonMaskLogEncoder() {
super();
customPatternMaskingLayout = new CustomPatternMaskingLayout();
maskEnabled = true;
}
@Override
public byte[] encode(ILoggingEvent event) {
return maskEnabled ? getMaskedJson(event) : super.encode(event);
}
private byte[] getMaskedJson(ILoggingEvent event) {
final Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger(event.getLoggerName());
final String message = customPatternMaskingLayout.maskMessage(event.getFormattedMessage());
final LoggingEvent loggingEvent =
new LoggingEvent(
"", logger, event.getLevel(), message, getThrowable(event), event.getArgumentArray());
return super.encode(loggingEvent);
}
private Throwable getThrowable(ILoggingEvent event) {
return event.getThrowableProxy() == null ? null : new Throwable(getStackTrace(event));
}
private String getStackTrace(ILoggingEvent event) {
final ExtendedThrowableProxyConverter throwableConverter =
new ExtendedThrowableProxyConverter();
throwableConverter.start();
final String errorMessageWithStackTrace = throwableConverter.convert(event);
throwableConverter.stop();
return errorMessageWithStackTrace;
}
@SuppressWarnings("unused")
public void setEnableMasking(boolean enabled) {
this.maskEnabled = enabled;
}
@SuppressWarnings("unused")
public void setPatterns(Patterns patterns) {
customPatternMaskingLayout.addMaskPatterns(patterns);
}
public static class Patterns extends ArrayList<String> {
@SuppressWarnings("unused")
public void addPattern(String pattern) {
add(pattern);
}
}
}
下面是实际CustomPatternMaskingLayout的代码:
package com.demo.config;
import static java.lang.String.format;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class CustomPatternMaskingLayout {
private Pattern multilinePattern;
private final List<String> maskPatterns = new ArrayList<>();
public CustomPatternMaskingLayout() {
compilePattern();
}
void addMaskPatterns(CustomJsonMaskLogEncoder.Patterns patterns) {
maskPatterns.addAll(patterns);
compilePattern();
}
private void compilePattern() {
multilinePattern = Pattern.compile(String.join("|", maskPatterns),Pattern.MULTILINE);
}
String maskMessage(String message) {
if (multilinePattern == null) {
return message;
}
StringBuilder sb = new StringBuilder(message);
Matcher matcher = multilinePattern.matcher(sb);
while (matcher.find()) {
IntStream.rangeClosed(1, matcher.groupCount()).forEach(group -> {
if (matcher.group(group) != null) {
IntStream.range(matcher.start(group), matcher.end(group)).forEach(i -> sb.setCharAt(i, '*'));
}
});
}
return sb.toString();
}
}
希望这有帮助!!