Spring Boot - WebClient Proxy

Murphy·2023년 3월 23일
0

어느날 갑자기

참여하고 있는 프로젝트의 Authorization 서비스에서 403 Forbidden을 응답하기 시작했다. 배포된 서비스는 DEV~PROD 스택까지 문제가 없다. Local에서만 문제가 나오고, WebClient를 통해 upstream 서비스에 접근하려고 하면 토큰을 받지 못해 아무 것도 안된다.

마침 사내 인프라쪽 문제가 있어서 프록시 서버 문제인가 했는데, 복구가 되어도 동작하지 않는다. 같은 요청을 Postman에서 프록시를 통해 보내면 된다.

아, 코드에 문제가 있다.

환경

프로젝트 환경은 다음과 같다.

  • Java 17
  • Spring Boot 3.0.4
    • spring-webflux 6.0.6
    • reactor-netty-http 1.14

원인

원인은 다양 할 수 있다. 내가 제어할 수 있는 부분이 있고, 내가 제어 할 수 없는 부분이 있을 뿐.

Authorization 서비스 혹은 프록시 변경

종종 프록시 서비스 이상으로 등적 서비스 혹은 인프라에 접금이 안되는 경우가 있다. 그리고 최근 보안정책 강화로 개발용 스택에 배포된 서비스의 접근 제어가 강화되고 있는 추세이다. Postman에서 프록시 서버를 거쳐 호출하는데 문제가 없으니, 일단 서비스 문제로 생각 해 볼 수 있다.

서비스 구현의 문제

당연한 건 없다.

이전에 동작했다고, 지금도 동작하라는 보장은 없다. 우선 현상을 보자.

Case 1. Authorization 서비스에서 scope을 받아오지 못 한다. (중요하지 않음)

퍼미션 확인을 위해 Authorization 서비스에서 scope을 받아오는 과정이 security filter에 있다. 요청을 보내면 403 Forbidden이 온다.
403 Forbidden 으로 프록시 문제를 유추하긴 어려웠다. 하지만 내가 보내는 client credential이 권한이 없는건 아니다. 먼저, Postman에서 프록시를 빼고 요청을 보냈다. 같은 오류가 발생한다.
Authorization 서비스의 접근제어에 대해 자세히 알 수 없지만, allow list가 존재하는 것 같다.

위 로직은 Authorization 서비스 팀에서 제공한 라이브러리를 통해서 진행 된다. 구현을 내가 변경 할 수 없고, 문서에 프록시 설정이 있는지 확인해 보았다.

있다.

문서에 나온 가이드 대로 프록시 정보를 넘겨 주도록 설정하니 이 부분은 잘 넘어간다.

Case 2. WebClient를 통해 upstream 서비스 접근시 발생하는 에러.

Case 1과 유사하다. WebClient를 통해 upstream 서비스에 접근하기 위해서는 authorization 서비스에서 token을 받아 사용하도록 설정되어 있다. 따라서 POST {authz}/openid/token API에 접근해야 하는데 위와 같이 403 Forbidden이 응답으로 오고 있다. 위 케이스처럼 프록시 문제로 생각했고 WebClient에서 token을 받는 로직에 프록시가 적용되지 않은 것으로 파악했다.

WebClient의 Token 처리

서비스에서 사용하고 있는 WebClient는 다양한 방법으로 Authorization header를 지정한다. 그 중 지정한 clientRegistrationId 정보를 기반으로 client_credential type의 token을 받아 자동으로 할당해 주는 방법도 있다. Token을 받기 위해서는 제공된 clientIdclientSecret 그리고 issuer정보를 통해 처리하게 된다.

다음은 WebClient 설정의 일부이다.

        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2ClientFilter =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                        authorizedClientManager
                );

        oauth2ClientFilter.setDefaultClientRegistrationId(clientRegistrationId);
        oauth2ClientFilter.setDefaultOAuth2AuthorizedClient(false);

        builder.filter(oauth2ClientFilter);

AuthorizedClientManager

WebClient 설정에 필요한 authorizedClientManager는 다음과 같이 구현 할 수 있다. authorizedClientManager는 다시 말해 인증 정보를 포함하고 있는 client를 관리하는 객체이다.

프록시가 설정된 httpClient를 사용하는 프로바이더를 생성해서 제공하는게 핵심이다.

  • createHttpClient()를 통해 프록시가 지정된 httpClient를 생성한다.
  • 사용할 인증 방법에 따른 프로바이더를 구현해 ReactiveOAuth2AuthorizedClientProvider를 설정한다.
  • 생성한 ReactiveOAuth2AuthorizedClientProviderAuthorizedClientManager에 지정해 준다.

authorizedClientManager 생성하기

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository
    ) {
        var authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository)
        );

        ReactorClientHttpConnector reactorClientHttpConnector = new ReactorClientHttpConnector(createHttpClient());

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .provider(getClientCredentialsReactiveOAuth2AuthorizedClientProvider(reactorClientHttpConnector))
                        .provider(getRefreshTokenReactiveOAuth2AuthorizedClientProvider(reactorClientHttpConnector))
                        .build();

        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;

    }

Proxy가 설정된 httpClient

    private HttpClient createHttpClient() {
        var httpClient =  HttpClient.create();
        
        if (StringUtils.isNotBlank(System.getProperty(HTTP_PROXY_HOST)) &&
                StringUtils.isNotBlank(System.getProperty(HTTP_PROXY_PORT)) &&
                StringUtils.isNumeric(System.getProperty(HTTP_PROXY_PORT))
        ) {
            httpClient = httpClient
                    .proxy(proxy -> proxy
                            .type(ProxyProvider.Proxy.HTTP)
                            .host(System.getProperty(HTTP_PROXY_HOST))
                            .port(Integer.parseInt(System.getProperty(HTTP_PROXY_PORT)))
                    );
        } else {
            httpClient = httpClient.noProxy();
        }

        return httpClient;
    }

Providers

내 경우에는 두 개의 프로바이더를 생성해서 제공했다. 두 개의 생성 과정은 큰 차이는 없고 생성하는 객체의 타입만 다르다.

  • ClientCredentialsReactiveOAuth2AuthorizedClientProvider
  • RefreshTokenReactiveOAuth2AuthorizedClientProvider
    private static ClientCredentialsReactiveOAuth2AuthorizedClientProvider getClientCredentialsReactiveOAuth2AuthorizedClientProvider(
            ReactorClientHttpConnector reactorClientHttpConnector
    ) {
        var clientCredentialsTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
        clientCredentialsTokenResponseClient.setWebClient(
                WebClient.builder()
                        .clientConnector(reactorClientHttpConnector)
                        .build()
        );

        var clientCredentialsReactiveOAuth2AuthorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
        clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(clientCredentialsTokenResponseClient);
        return clientCredentialsReactiveOAuth2AuthorizedClientProvider;
    }

정리하며

회사에서 개발하다 보면 이런 네트워크 정책 변경으로 문제가 발생하는 경우가 종종 있다. 처음에는 문제가 자동적으로 수정되길 기대했는데, 역시 바뀐 정책은 돌아오지 않는다.
문제 해결을 위해서 알아보다 보면 처음부터 이런식으로 구현되었어야 하는거 아닌가 싶기도 하다. 생성된 WebClient에는 프록시 설정을 했으면서 그 WebClient가 token을 받는 과정에는 프록시 설정을 하지 않았다. 다시 생객해보면 당연히 했어야 한다. 그저 운이 좋아서 Authorization 서비스가 회사 네트워크와 VPN에서 그냥 동작 했던 것이다.

당연한 건 없다.

profile
Anything that can go wrong will go wrong.

0개의 댓글