참여하고 있는 프로젝트의 Authorization 서비스에서 403 Forbidden
을 응답하기 시작했다. 배포된 서비스는 DEV~PROD 스택까지 문제가 없다. Local에서만 문제가 나오고, WebClient
를 통해 upstream 서비스에 접근하려고 하면 토큰을 받지 못해 아무 것도 안된다.
마침 사내 인프라쪽 문제가 있어서 프록시 서버 문제인가 했는데, 복구가 되어도 동작하지 않는다. 같은 요청을 Postman에서 프록시를 통해 보내면 된다.
아, 코드에 문제가 있다.
프로젝트 환경은 다음과 같다.
원인은 다양 할 수 있다. 내가 제어할 수 있는 부분이 있고, 내가 제어 할 수 없는 부분이 있을 뿐.
종종 프록시 서비스 이상으로 등적 서비스 혹은 인프라에 접금이 안되는 경우가 있다. 그리고 최근 보안정책 강화로 개발용 스택에 배포된 서비스의 접근 제어가 강화되고 있는 추세이다. Postman에서 프록시 서버를 거쳐 호출하는데 문제가 없으니, 일단 서비스 문제로 생각 해 볼 수 있다.
이전에 동작했다고, 지금도 동작하라는 보장은 없다. 우선 현상을 보자.
퍼미션 확인을 위해 Authorization 서비스에서 scope을 받아오는 과정이 security filter에 있다. 요청을 보내면 403 Forbidden
이 온다.
403 Forbidden
으로 프록시 문제를 유추하긴 어려웠다. 하지만 내가 보내는 client credential이 권한이 없는건 아니다. 먼저, Postman에서 프록시를 빼고 요청을 보냈다. 같은 오류가 발생한다.
Authorization 서비스의 접근제어에 대해 자세히 알 수 없지만, allow list가 존재하는 것 같다.
위 로직은 Authorization 서비스 팀에서 제공한 라이브러리를 통해서 진행 된다. 구현을 내가 변경 할 수 없고, 문서에 프록시 설정이 있는지 확인해 보았다.
있다.
문서에 나온 가이드 대로 프록시 정보를 넘겨 주도록 설정하니 이 부분은 잘 넘어간다.
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을 받기 위해서는 제공된 clientId
와 clientSecret
그리고 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
를 설정한다.ReactiveOAuth2AuthorizedClientProvider
를 AuthorizedClientManager
에 지정해 준다.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;
}
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;
}
내 경우에는 두 개의 프로바이더를 생성해서 제공했다. 두 개의 생성 과정은 큰 차이는 없고 생성하는 객체의 타입만 다르다.
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에서 그냥 동작 했던 것이다.
당연한 건 없다.