Logback PatternLayout을 통한 로그 마스킹 처리 방법
Logback PatternLayout을 통한 로그 마스킹 처리 방법(LayoutWrappingEncoder, PatternLayoutEncoder)
'개인정보의 암호화' 등 관련된 법령에 따라 비밀번호, 주민등록번호 등은 저장 시에 반드시 암호화하여 저장해야 합니다.
그리고 만약 시스템에서 요청 파라미터에 대한 로그를 남기고 있을 경우 암호화 처리 전의 비밀번호, 주민등록번호 등이 로그 파일에 남을 수 있다는 점 또한 주의해야 하는데요.
해당 포스팅에서는 logback 로깅 라이브러리 사용 시 PatternLayout 클래스를 통한 로그 마스킹 처리 방법에 대해 알아보고, 더불어 Encoder, LayoutWrappingEncoder, PatternLayoutEncoder 등에 대한 개념에 대해서도 정리해 보았습니다.
(내용 흐름은 로그 마스킹 처리에 대한 전체적인 소스 코드부터 먼저 살펴본 뒤, 이어서 주요 개념들에 대해서 살펴보도록 하겠습니다.)
PatternLayout을 통한 비밀번호, 주민등록번호 마스킹 처리 방법
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
public class MaskingPatternLayout extends PatternLayout {
private Pattern multilinePattern;
private List<String> maskPatterns = new ArrayList<>();
public void addMaskPattern(String maskPattern) {
maskPatterns.add(maskPattern);
multilinePattern = Pattern.compile(maskPatterns.stream().collect(Collectors.joining("|")), Pattern.MULTILINE);
}
@Override
public String doLayout(ILoggingEvent event) {
return maskMessage(super.doLayout(event));
}
private 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();
}
}
먼저 ch.qos.logback.classic.PatternLayout 클래스를 상속받은 MaskingPatternLayout 클래스를 구현합니다.
logback의 경우 생성자 주입을 지원하지 않기 때문에 다음과 같이 addMaskPattern() 메서드를 통해 logback.xml 파일에 정의된 각각의 maskPattern 요소를 layout에 주입하게 됩니다.
그리고 doLayout() 메서드의 경우 로깅 이벤트가 발생했을 때, 해당 로그 메시지에 대해 layout에 주입된 maskPattern 중 일치하는 것이 있는지 확인 후 일치하는 패턴이 존재하는 경우 해당 값을 마스킹하는 역할을 수행합니다.
(37번 라인을 통해 패턴에 해당되는 부분을 '*' 문자로 마스킹 처리하고 있으며, 이 부분의 수정을 통해 마스킹되는 문자를 변경할 수 있습니다.)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- property 값 설정 -->
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{35} - %msg%n" />
<!-- Console Appender -->
<appender name="ConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<!-- 출력 패턴 설정 -->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.spring.practice.config.MaskingPatternLayout">
<maskPattern>(([0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|[3][01]))\-([1-4][0-9]{6}))</maskPattern> <!-- 주민등록번호 maskPattern -->
<maskPattern>password\s*=\s*([^,)\s]+)</maskPattern> <!-- (map || dto) password maskPattern -->
<maskPattern>\"password\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern> <!-- (JSON) password maskPattern -->
<pattern>${LOG_PATTERN}</pattern>
</layout>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
</root>
</configuration>
logback.xml 파일의 주요 부분은 다음과 같이 구성되어 있는데요.
encoder로는 LayoutWrappingEncoder를 사용하였고, layout은 위에서 구현한 MaskPatternLayout을 명시하였습니다.
(com.spring.practice.config는 현재 프로젝트 기준의 예시이며 MaskPatternLayout 클래스가 구현된 패키지 경로를 명시해 주시면 됩니다.)
핵심이 되는 'maskPattern'의 경우 마스킹 처리할 대상에 맞춰 구현하면 되는데요.
주의할 점으로는 '주민등록번호(rrn)'와 같은 규격화된 형식이 있는 마스킹 대상 같은 경우에는 해당 형식을 정규표현식으로 적용시키면 되지만 '비밀번호(password)'와 같이 규격화된 형식이 없는 대상을 마스킹하려는 경우 해당 parameter의 key 값을 포함하여 정규표현식을 적용시켜야 한다는 점이 있습니다.
// 마스킹 처리 전 로그
example Map: {password=test123!@#, id=user01, rrn=900101-1234567}
example JSON: {"password":"test123!@#","id":"user01","rrn":"900101-1234567"}
example Dto: UserDto [id=user01, password=test123!@#, rrn=900101-1234567]
// 마스킹 처리 후 로그
example Map: {password=**********, id=user01, rrn=**************}
example JSON: {"password":"**********","id":"user01","rrn":"**************"}
example Dto: UserDto [id=user01, password=**********, rrn=**************]
Encoder
Encoder는 logback 0.9.19 버전부터 추가된 기능으로, 0.9.19 이전 버전에서는 Layout을 사용하여 이벤트를 문자열로 변환하고 java.io.Writer를 통해 쓰도록 동작되었는데요.
Layout의 경우 로깅 이벤트를 오직 문자열로만 변환할 수 있다는 제한사항이 있었기 때문에 Encoder가 도입되었으며, Encoder는 로깅 이벤트를 바이트 배열(byte array)로 변환하고, 해당 바이트 배열을 OutputStream에 쓰는 역할을 수행합니다.
LayoutWrappingEncoder
package ch.qos.logback.core.encoder;
public class LayoutWrappingEncoder<E> extends EncoderBase<E> {
protected Layout<E> layout;
private Charset charset;
public byte[] encode(E event) {
String txt = layout.doLayout(event);
return convertToBytes(txt);
}
private byte[] convertToBytes(String s) {
if (charset == null) {
return s.getBytes();
} else {
return s.getBytes(charset);
}
}
}
Encoder에서 언급한 것처럼 logback 0.9.19 이전 버전에서는 Layout을 통해 로그 이벤트를 문자열로 변환하였는데요.
때문에 Encoder가 도입되었지만, 대부분의 소스 코드에서는 Layout 인터페이스를 기반으로 사용하고 있었기 때문에 Layout과 Encoder를 연결할 수 있는 것이 필요했습니다.
이 역할을 하는 것이 바로 LayoutWrappingEncoder이며, LayoutWrappingEncoder는 encoder 인터페이스를 구현하고 내부에 Layout을 가지고 있어 로깅 이벤트를 문자열로 변환하는 작업을 Layout에게 위임하게 됩니다.
위 코드에서 encode() 메서드를 보면 이벤트를 layout.doLayout() 메서드를 통해 로깅 이벤트를 문자열로 변환하고, 그 결과를 다시 바이트 배열로 바꾸는 것을 볼 수 있습니다.
PatternLayoutEncoder
PatternLayout은 가장 대표적으로 사용되는 layout이며, logback은 기존의 PatternLayout을 대체하기 위해 LayoutWrappingEncoder를 상속하고 PatternLayout을 Wrapping 한 PatternLayoutEncoder를 제공하였습니다.
(PatternLayout 외에 layout으로는 HTMLLayout, XMLLayout이 있습니다.)
때문에 logback 0.9.19 버전 이후에 FileAppender 또는 FileAppender의 서브 클래스는 PatternLayout을 설정하기 위해 PatternLayoutEncoder를 사용해야 합니다.
<!-- 0.9.19 버전 이전 PatternLayout을 사용하는 예시 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>file.log</file>
...
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%msg%n</pattern>
</layout>
</appender>
<!-- 0.9.19 버전 이후 PatternLayoutEncoder를 사용하는 예시 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>file.log</file>
...
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%msg%n</pattern>
</encoder>
</appender>
< 참고 자료 >
https://www.baeldung.com/logback-mask-sensitive-data
https://logback.qos.ch/manual/encoders.html
https://ckddn9496.tistory.com/83