LG CNS 부트캠프 학습일지 56일차
학습 내용
- 마이크로서비스를 컨테이너로 만들기
- 특정 주소를 통해 접근할 때 인증이 안되던 문제 해결
- 스프링부트 어플리케이션의 로그 레벨 설정 방법
개요
어제와 마찬가지로 User Service, Order Service, Catalog Service 등을 컨테이너로 만드는 실습을 했다. 그 과정에서 오류가 발생했었는데, 문제를 해결하는 과정을 기록하려고 합니다.
특정 주소를 통해 접근할 때 인증이 안되던 문제 해결
회원가입 및 로그인이 안되는 문제가 있었다. 개발환경에서 실행했을 때는 문제가 없었는데 컨네이터로 만들어서 실행하니까 문제가 발생했다.
1
2
3
4
5
6
# 1. localhost
curl --request POST http://localhost:8081/users --header "Content-Type: application/json" --data @user-registration.json
# 2. 127.0.0.1
curl --request POST http://127.0.0.1:8081/users --header "Content-Type: application/json" --data @user-registration.json
# 3. 호스트의 IP 주소
curl --request POST http://172.30.1.7:8081/users --header "Content-Type: application/json" --data @user-registration.json
1번과 2번의 경우는 사실상 동일하다. 아무튼 localhost 를 통해서 컨테이너에 요청을 보내면 401 권한 오류가 발생했다. User Service의 WebSecurity 클래스는 아래와 같이 정의되어 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@EnableWebSecurity
public class WebSecurity {
...
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
...
http.csrf( (csrf) -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/health-check/**").permitAll()
.requestMatchers("/welcome/**").permitAll()
.requestMatchers("/**").access(
new WebExpressionAuthorizationManager(
"hasIpAddress('127.0.0.1')"
+ " or hasIpAddress('::1')"
+ " or hasIpAddress('172.30.1.0/24')"
+ " or hasIpAddress('::1')"))
.anyRequest().authenticated()
)
.authenticationManager(authenticationManager)
.addFilter(getAuthenticationFilter(authenticationManager))
.httpBasic(Customizer.withDefaults())
.headers((headers) -> headers
.frameOptions((frameOptions) -> frameOptions.sameOrigin()));
return http.build();
}
...
}
localhost 호스트명의 IP 주소는 127.0.0.1 이고 따라서 인증하지 않고도 접근할 수 있어야 한다. 하지만 도커 컨테이너가 인식하는 요청의 IP 주소는 127.0.0.1 이 아니었다. 이를 확인하기 위해서 필터를 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
log.info("{} {} from {}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr());
filterChain.doFilter(request, response);
}
}
다른 필터보다 먼저 실행하도록, 정확히는 인증필터 (AuthenticationFilter) 보다 먼저 실행이되도록 우선순위를 조정했다. 순서를 명시하지 않으면 Ordered.LOWEST_PRECEDENCE 값으로 정해지고, 인증필터가 요청을 거절하면 요청에 대한 정보를 출력하지 못하는 문제가 있었다.
소스코드를 수정하고 curl 명령어를 사용해서 요청을 보내봤다. 호스트의 주소인 172.30.1.7로 요청을 보냈을 때는 172.30.1.7 에서 요청을 보낸 것으로 인식했었다. 하지만 localhost(127.0.0.1) 로 요청을 보내면 192.168.176.1 에서 요청을 보낸 것으로 인식했었다.
그 이유는 도커 브릿지 네트워크를 사용하고 있기 때문이었다. localhost 를 사용해서 컨테이너에 접근하면 브릿지 네트워크는 그 요청을 브릿지 네트워크의 게이트웨이에서 보낸 것으로 변환한다. docker network inspect 명령어로 브릿지 네트워크의 게이트웨이 주소를 확인해보니 192.168.176.1 와 동일했다.
이 문제를 해결하기 위해서 WebSecurity 클래스를 수정해서 POST /users 엔드포인트에 대한 접근을 조건없이 허용하도록 수정했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@EnableWebSecurity
public class WebSecurity {
...
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
...
http.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/health-check/**").permitAll()
.requestMatchers("/welcome/**").permitAll()
.requestMatchers(HttpMethod.POST, "/users").permitAll()
.requestMatchers("/**").access((authentication, context) -> {
...
})
)
.authenticationManager(authenticationManager)
.addFilter(getAuthenticationFilter(authenticationManager))
.httpBasic(Customizer.withDefaults()) // ← Basic 인증 추가
.headers((headers) -> headers
.frameOptions((frameOptions) -> frameOptions.sameOrigin()));
return http.build();
}
...
}
API를 이렇게 설계하는 것이 안전한지는 모르겠다. 한편으로는 지나치게 걱정하는 것일 수도 있겠다고 생각했는데, 어차피 컨테이너 환견에서는 호스트로의 접근부터 제한이 되고 그리고 회원가입에 해당하는 POST /users 엔드포인트는 어디서든 접근이 가능하도록 공개하는 것이 타당하기 때문이다.
아무튼 문제를 해결하면서 인증필터와 도커 네트워크에 대해서 내가 무엇을 잘못알고 있었는지 알 수 있었다.
스프링부트 어플리케이션의 로그 레벨 설정 방법
위에서 RequestLogginFilter 안에서 info() 메소드를 사용해서 로그 메세지를 출력하도록 만들었다.
1
log.info("{} {} from {}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr());
하지만 처음에는 debug() 메소드를 사용했었는데, 로그 메세지가 출력되지 않아서 필터 자체에 문제가 있었는지 확인하느라 헤맸었다. 결론부터 말하면 로그 레벨을 어떻게 설정되어 있는지 그리고 어떻게 설정해야하는지 몰랐기 때문이었다.
스프링부트는 기본적으로 INFO 로그 레벨을 사용한다. 무슨 말이냐면 DEBUG 레벨의 메세지는 출력되지 않는다는 것이다. DEBUG 레벨 메세지를 출력하려면 application.yml 파일에서 로그 레벨을 수정해주어야 했다. 만약 RequestLoggingFilter 와 같이 com.example.userservice.security 패키지 아래에 있는 클래스에서 로그 레벨을 DEBUG로 하고 싶다면 아래와 같이 하면 된다.
1
2
3
4
5
logging:
level:
root: INFO # default
org.springframework.security: DEBUG
com.example.userservice.security: DEBUG
Comments powered by Disqus.