Container화 한 Spring Boot 3의 baseUrl을 Nginx header로 알맞게 바꿔주자!
OAuth2 Redirect URI 관련 문제 발생
이번 프로젝트는 Nginx로 도커 환경으로 서버를 분리된 서버에 전달되도록 구성했습니다. 그래서 application의 {baseUrl}이 자동으로 서버 Url로 설정이 될 줄 알았는데 Nginx가 앞에 있어서 실제 유저가 접속할 때 쓰는 Url과 다른 Url이 되는 것을 알게 되었습니다. {baseUrl}이 이상하니 이 주소 기반으로 된 OAuth2 redirect-uri 설정도 문제가 생겼습니다. 배포 파이프라인을 수정할까 했는데 생각보다 깔끔하게 해결할 수 있었습니다. 이걸 해결하며 발견한 것들을 공유해보고자 합니다.
현재 서버 구성
현재 개발용과 프로덕션용으로 Spring Boot 애플리케이션 2개를 하나의 서버에 컨테이너화하여 도커로 배포하고 있습니다.
(비용을 위해 하나의 서버에 모두 띄우고 nginx로 프록시하도록 했습니다.)
개발된 서버들이 이렇게 docker 컨테이너로 띄워져있습니다. 컨테이너들은 localhost만 listen하고 있고 Nginx 뒤에 있습니다.
443(프로덕션용)이나 8443(개발용)으로 온 HTTPS 요청은 먼저 Nginx에서 처리합니다. Nginx가 TLS 처리를 하고 http 패킷 형태로 header를 추가해서 컨테이너로 전달합니다.
그림으로 보면 다음과 같습니다.
문제: 서버 설정 값
아래는 스프링 부트에서 쓰고 있는 application.properties 중 일부입니다.
spring:
security:
oauth2:
client:
registration:
kakao:
# 생략
redirect-uri: "{baseUrl}/login/oauth2/code/kakao"
provider:
kakao:
# 생략
여기서 문제가 되는 부분은 redirect-uri입니다. baseUrl이 환경이 컨테이너이기 때문에 Prod나 Dev Container 안의 스프링 애플리케이션은 자신의 baseUrl이 localhost:8080이라고 생각했습니다. 이걸 해결하기 위해 이리저리 문서랑 해결책을 찾아보았고 nginx를 이용해서 깔끔하게 해결할 수 있는 방법을 찾았습니다!
해결법
1. Nginx에 커스텀 헤더 설정
어쨌든 어떤 환경에서 왔는지 알려면 Nginx에서 받은 정보가 필요합니다. 그래서 Nginx에게 다음과 같이 커스텀 header를 통해 어떤 프로토콜, IP, HOST, Protocol로 왔는지 전달하도록 합니다.
그러면 컨테이너 안의 스프링부트 애플리케이션에서도 커스텀 헤더를 통해 자신이 실제로 어떤 Url로 배포되는지 알 수 있습니다.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port
과연 그런가 테스트 해보기
nginx가 필요한 header를 붙이고 전달한 패킷은 HTTP이기 때문에 tcpdump로 간단하게 확인해볼 수 있겠다 싶어서 tcpdump를 해서 내용을 살펴봤습니다.
클라이언트에서 HTTP 요청을 이렇게 보냈습니다.
GET /api/test HTTP/1.1
Accept: */*
Host: api.xxxxx.com:8443
User-Agent: HTTPie
Nginx가 받고 스프링 서버로 전달해준 HTTP 요청은 최종적으로 다음과 같았습니다.
실제 서빙된 호스트 주소, 포트, 프로토콜을 header 값으로 확인할 수 있었고 이걸로 잘 작동한다는 것을 확인했습니다.
2. 스프링 애플리케이션 수정
이제 이 정보를 기반으로 spring application이 baseUrl을 설정하도록 하면 됩니다.
이전에 파이썬에서 비슷한 설정을 해본 적이 있어서 당연히 스프링에도 있을 것이라 생각했는데 아니나 다를까 스프링 공식 문서에 프록시 서버에 대한 가이드가 있어서 금방 해결법을 찾을 수 있었습니다.
말고도 다른 글도 찾아 보면서 ForwardHeadersStrategy 설정을 통해 가능하다는 것을 알았습니다. 이때 X-Forwarded-* header를 처리하는 방식으로 native와 framework 두 가지 Strategy가 지원된다는 것을 알았습니다.
이것을 사용하려면 application.properties에 server.forward-headers-strategy 값을 설정하면 됩니다.
# native headers strategy
server.forward-headers-strategy=native # 혹시 framework
선택지 1. Native ForwardHeadersStrategy
서블릿 컨테이너(Tomcat, Jetty 등)의 기본 기능을 활용해서 X-Forwarded-* header를 처리하는 방식입니다. 성능이 좋지만 일부 비표준 header는 지원하지 않는다고 합니다.
저는 특이한 헤더명을 쓰지 않고 common한 header들로 충분했습니다. 그래서 위의 공식 문서에서 권고한대로 native로 충분해서 이 설정을 선택했습니다.
Native 방식은 application.yaml에 RemoteIpValve를 Trace 레벨로 로그를 찍도록 아래처럼 설정하면 실제로 프록시에서 설정한 헤더로 온 값들을 볼 수 있습니다.
아래처럼 설정하고
# native headers strategy logging
logging.level.org.apache.catalina.valves.RemoteIpValve=trace
다음처럼 프록시 헤더를 몇 개 설정해서 요청을 로컬에 띄워둔 서버에 보내봤습니다.
GET /login/oauth2/code/kakao HTTP/1.1
Host: groot-example-domain.com
X-Forwarded-Port: 9999
X-Forwarded-Proto: https
아래처럼 호스트와 프로토콜과 포트들이 넘어왔음을 애플리케이션에서 볼 수 있었습니다.
선택지 2. Framework ForwardHeadersStrategy
Spring Framework에서 제공하는 ForwardedHeaderFilter를 사용하여 헤더를 처리하는 방식입니다. 만약 비표준 헤더까지 완벽하게 지원해야 하면 좋은 선택지인 것 같습니다. 하지만 애플리케이션에서 처리되니 약간의 성능 오버헤드가 있을 수 있다고 합니다.
Spring Boot에서 헤더를 해석해서 baseUrl이 잘 적용됐는지 확인해보기
실제로 애플리케이션에서 이 프록시로 넘어온 헤더들을 기반으로 redirect-url이 잘 설정되는지 궁금했습니다.
그래서 매번 Request마다 Spring Boot에서 헤더를 해석해서 baseUrl이 잘 설정했는지 oauth redirect-url을 찍어 봤습니다.
새로운 OAuth2 인증 요청이 처리될 때마다 최종적으로 결정된 redirect 주소를 로깅하기 위해 다음과 같이 로깅용 Request Resolver를 만듭니다.
// 생략
@Slf4j
public class LoggingOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver delegate;
public LoggingOAuth2AuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
this.delegate = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = delegate.resolve(request);
if (req != null) {
log.info("Resolved redirect URI: {}", req.getRedirectUri()); // 실행될 때 req의 redirectUri를 로그를 남긴다.
}
return req;
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = delegate.resolve(request, clientRegistrationId);
if (req != null) {
log.info("Resolved redirect URI: {}", req.getRedirectUri());
}
return req;
}
}
그리고 기존의 Security config에 등록해줍니다. 간단하게 로그만 보면 돼서 login 아래 모든 경로(/login/**)에 적용되도록 했습니다.
// 생략
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
// 생략
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
validateDependencies();
return http
// 생략
.oauth2Login(
oauth2 -> oauth2.authorizationEndpoint(authEndPoint ->
authEndPoint.authorizationRequestResolver(new LoggingOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/login/**")))) // 위의 resolver 클래스를 등록
// 생략
.build();
}
}
이렇게 애플리케이션 단에서 로그를 설정하고 직접 로컬에서 띄워서 X-Forwarded-* 헤더들을 조작해가며 다음과 같은 요청해봤습니다.
GET /login/oauth2/code/kakao HTTP/1.1
Host: groot-example-domain.com
X-Forwarded-Port: 9999
X-Forwarded-Proto: https
GET /login/oauth2/code/kakao HTTP/1.1
Host: test2.com
X-Forwarded-Port: 8888
X-Forwarded-Proto: http
GET /login/oauth2/code/kakao HTTP/1.1
Host: test3.com
X-Forwarded-Port: 2222
X-Forwarded-Proto: ftp
서버에 출력된 로그는 다음과 같이 잘 나왔습니다.
이로써 실제 서버에서 헤더로 처리하고 resolve된 redirect URI를 보며 어떻게 baseUrl이 변하는지 명확히 확인할 수 있었습니다.
이제 Nginx 헤더 설정이 걸리는 부분이 있어서 보안적으로 위험한 부분은 없는 검토해볼 것 같습니다. 가능하면 변조가 불가능하게 서비스 헤더값을 하드코딩하는 것도 좋을 것 같네요 :)
댓글
이 글 공유하기
다른 글
-
캐시
캐시
2025.03.20 -
폰 노이만 구조의 특징
폰 노이만 구조의 특징
2025.03.20 -
Error installing cocoapods 해결 후, CocoaPods 업그레이드하는 법
Error installing cocoapods 해결 후, CocoaPods 업그레이드하는 법
2023.11.17 -
Flutter3.0 Firebase 연동하기
Flutter3.0 Firebase 연동하기
2023.10.05