[java] OAuth2RestTemplate의 Spring Security 5 교체

에서 spring-security-oauth2:2.4.0.RELEASE같은 클래스 OAuth2RestTemplate, OAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvider사용되지 않는 모든 표시되었습니다.

이 클래스의 javadoc에서 사람들이 핵심 스프링 보안 5 프로젝트로 마이그레이션해야 함을 알려주 는 스프링 보안 마이그레이션 안내서 를 가리 킵니다 . 그러나이 프로젝트에서 사용 사례를 구현하는 방법을 찾는 데 문제가 있습니다.

응용 프로그램에 대한 들어오는 요청을 인증하고 타사 OAuth 공급자를 사용하여 ID를 확인하려는 경우 모든 설명서 및 예제는 타사 OAuth 공급자와의 통합에 대해 설명합니다.

유스 케이스 RestTemplate에서 OAuth로 보호되는 외부 서비스를 요청 하기 만하면됩니다. 현재 나는 OAuth2ProtectedResourceDetails고객 ID와 비밀을 사용하여로 전달합니다 OAuth2RestTemplate. 나는 또한 사용자는 한 ClientCredentialsAccessTokenProvider에 추가 된 OAuth2ResTemplate단지 내가 사용하고있어 OAuth를 제공에 필요한 토큰 요청에 몇 가지 추가 헤더를 추가하는.

spring-security 5 문서 에서 토큰 요청 사용자 정의 를 언급하는 섹션을 찾았 지만 다시 타사 OAuth 제공자와 수신 요청을 인증하는 맥락에서 보입니다. ClientHttpRequestInterceptor외부 서비스에 대한 각 발신 요청이 먼저 토큰을 얻은 다음 요청에 추가되도록하기 위해 이것을 어떻게 a와 결합하여 사용하는지는 확실하지 않습니다 .

또한 위에 링크 된 마이그레이션 안내서에는 OAuth2AuthorizedClientService인터셉터에서 사용하는 데 유용한 것으로 언급되어 있지만 다시 ClientRegistrationRepository사용하려면 타사 공급자의 등록을 유지하는 위치 에 의존 하는 것처럼 보입니다. 수신 요청이 인증되도록합니다.

애플리케이션의 발신 요청에 토큰을 추가하기 위해 OAuth 제공자를 등록하기 위해 spring-security 5의 새로운 기능을 사용할 수있는 방법이 있습니까?



답변

OAuth는 2.0 클라이언트는 지원하지 않습니다 5.2.x 봄 보안의 기능을 RestTemplate하지만, WebClient. 보기 봄 보안 참조 :

HTTP 클라이언트 지원

  • WebClient 서블릿 환경을위한 통합 (보호 자원 요청을 위해)

또한 RestTemplate향후 버전 에서는 더 이상 사용되지 않습니다. RestTemplate javadoc을 참조하십시오 .

참고 : 비 블로킹 반응 형 5.0에서는
스트리밍 시나리오뿐만 아니라 동기화 및 비동기 모두에 대한 효율적인 지원을 org.springframework.web.reactive.client.WebClient제공하는 현대적인 대안을 제공합니다 RestTemplate. 는 RestTemplate향후 버전에서 더 이상 사용되지 않습니다 및 향후 추가 된 새로운 주요 기능이 없습니다. 자세한 WebClient내용과 예제 코드는 Spring Framework 참조 문서 섹션을 참조하십시오 .

따라서 최선의 해결책은 RestTemplate에 찬성 하여 포기 하는 것입니다 WebClient.


사용 WebClient흐름 클라이언트 자격 증명에 대해

프로그래밍 방식으로 또는 Spring Boot 자동 구성을 사용하여 클라이언트 등록 및 제공자를 구성하십시오.

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

… 그리고 OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

제공된 것과 함께 WebClient사용할 인스턴스를 구성하십시오 .ServerOAuth2AuthorizedClientExchangeFilterFunctionOAuth2AuthorizedClientManager

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

이제이 WebClient인스턴스를 사용하여 요청을 시도 하면 먼저 권한 부여 서버에서 토큰을 요청하여 요청에 포함시킵니다.


답변

@Anar Sultanov의 위의 답변은이 시점까지 도달하는 데 도움이되었지만 OAuth 토큰 요청에 헤더를 추가해야 할 때 사용 사례의 문제를 해결하는 방법에 대한 완전한 답변을 제공 할 것이라고 생각했습니다.

제공자 세부 사항 구성

에 다음을 추가하십시오 application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

맞춤 구현 ReactiveOAuth2AccessTokenResponseClient

이것은 서버 간 통신이므로을 사용해야합니다 ServerOAuth2AuthorizedClientExchangeFilterFunction. 이것은 ReactiveOAuth2AuthorizedClientManager비 반응이 아닌을 허용합니다 OAuth2AuthorizedClientManager. 따라서 ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(제공자가 OAuth2 요청을하는 데 사용하도록) ReactiveOAuth2AuthorizedClientProvider사용하는 경우 비 반응성 대신에 이를 제공해야합니다 OAuth2AuthorizedClientProvider. 당으로 스프링 보안 참조 문서 당신이 사용하는 경우 비 반응 DefaultClientCredentialsTokenResponseClient당신이 사용할 수있는 .setRequestEntityConverter()으로 OAuth2 토큰 요청을 변경하는 방법을하지만, 반응 상응하는 WebClientReactiveClientCredentialsTokenResponseClient우리가 (우리가 사용을 자신 할 수있는 우리의 구현해야하므로,이 기능을 제공하지 않습니다 기존 WebClientReactiveClientCredentialsTokenResponseClient논리).

내 구현이 호출되었습니다 UaaWebClientReactiveClientCredentialsTokenResponseClient( 기본적 으로 headers()body()메소드를 약간 변경하여 WebClientReactiveClientCredentialsTokenResponseClient추가 헤더 / 본문 필드를 추가하기 때문에 구현이 생략되었으므로 기본 인증 흐름은 변경되지 않습니다).

구성 WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()메소드는 더 이상 사용되지 않으므로 해당 메소드에서 제공되지 않는 조언을 따르십시오.

더 이상 사용되지 않습니다. 대신 사용하십시오 ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager). (또는 사용자 정의 인스턴스)로 ClientCredentialsReactiveOAuth2AuthorizedClientProvider구성된 인스턴스를 작성하고에 WebClientReactiveClientCredentialsTokenResponseClient제공하십시오 DefaultReactiveOAuth2AuthorizedClientManager.

이것은 다음과 같은 구성으로 끝납니다.

@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository
    clientRegistrationRepository)
{
    final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
        clientCredentialsReactiveOAuth2AuthorizedClientProvider =
            new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
    clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
        new UaaWebClientReactiveClientCredentialsTokenResponseClient());

    final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
        new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
            new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
        clientCredentialsReactiveOAuth2AuthorizedClientProvider);

    final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
    oAuthFilter.setDefaultClientRegistrationId("uaa");

    return WebClient.builder()
        .filter(oAuthFilter)
        .build();
}

WebClient정상적으로 사용

이제 oAuth2WebClientBean을 사용하여 다른 요청을하는 방식으로 구성된 OAuth2 제공자가 보호하는 자원에 액세스 할 수 WebClient있습니다.


답변

@ matt Williams의 답변이 도움이된다는 것을 알았습니다. 누군가가 WebClient 구성을 위해 clientId 및 secret을 프로그래밍 방식으로 전달하려는 경우 추가하고 싶습니다. 다음은 완료 방법입니다.

 @Configuration
    public class WebClientConfig {

    public static final String TEST_REGISTRATION_ID = "test-client";

    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientId("<client_id>")
                .clientSecret("<client_secret>")
                .tokenUri("<token_uri>")
                .build();
        return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {

        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,  new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);

        return WebClient.builder()
                .baseUrl("https://.test.com")
                .filter(oauth)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}


답변

안녕하세요 아마 너무 늦었지만 RestTemplate은 Spring Security 5에서 여전히 지원됩니다.

client_credentials 플로우를 사용하려면 다음 구성을 사용하십시오.

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

OauthResTemplate에 대한 구성

@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {

    public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";

    private final RestTemplateBuilder restTemplateBuilder;
    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean(OAUTH_WEBCLIENT)
    RestTemplate oAuthRestTemplate() {
        var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);

        return restTemplateBuilder
                .additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
                .setReadTimeout(Duration.ofSeconds(5))
                .setConnectTimeout(Duration.ofSeconds(1))
                .build();
    }

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager() {
        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

요격기

public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final Authentication principal;
    private final ClientRegistration clientRegistration;

    public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
        this.principal = createPrincipal();
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(principal)
                .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }

        request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
        return execution.execute(request, body);
    }

    private Authentication createPrincipal() {
        return new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.emptySet();
            }

            @Override
            public Object getCredentials() {
                return null;
            }

            @Override
            public Object getDetails() {
                return null;
            }

            @Override
            public Object getPrincipal() {
                return this;
            }

            @Override
            public boolean isAuthenticated() {
                return false;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            }

            @Override
            public String getName() {
                return clientRegistration.getClientId();
            }
        };
    }
}

이것은 첫 번째 호출에서 그리고 토큰이 만료 될 때마다 access_token을 생성합니다. OAuth2AuthorizedClientManager가이 모든 것을 관리합니다


답변