Клиент Spring Security Oauth2 получает токен доступа с ошибкой с недопустимым кодом запроса = 415, сообщение = неподдерживаемый тип носителя

#java #spring-boot #spring-security-oauth2

#java #spring-boot #spring-security-oauth2

Вопрос:

Используя Spring Boot, я настроил Oauth2RestTemplate компонент в классе конфигурации и соответствующие свойства в файле свойств. Я использовал Swagger codegen для создания заглушки клиента. Когда я пытаюсь вызвать RESTful API, Spring не получает токен доступа, основной причиной которого является «неподдерживаемый тип носителя». Ниже приведена трассировка стека, моя конфигурация клиента Spring Security и мои попытки исправить. Любая помощь была бы высоко оценена!

Извлечение токена из https://dev-api.some-domain.com/auth/oauth2/v1/token
ClientCredentialsAccessTokenProvider.doWithRequest - форма кодирования и отправки: 
{grant_type=[client_credentials], scope=[read], client_id= [значение из реквизитов], client_secret= [значение из реквизитов]}

ошибка = "access_denied", error_description = "Токен доступа отклонен.
 в org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken (OAuth2AccessTokenSupport.java:142)
 в org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider.obtainAccessToken(ClientCredentialsAccessTokenProvider.java:44) 
 в org.springframework.security.oauth2.client.token.Доступ к сети.Получение нового доступа к сети внутренний (доступ к сети.java:148)
 в org.springframework.security.oauth2.client.token.Доступ к цепочке.Получение доступа к цепочке (AccessTokenProviderChain.java:121)
 в org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
 в org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken (OAuth2RestTemplate.java:173)
 в org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) 
 в org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735)
 в org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute (OAuth2RestTemplate.java:128) 
 в org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:651)
 в com.my.co.service.holidays.client.invoker.ApiClient.invokeAPI (ApiClient.java:518)
 в com.my.co.service.holidays.client.api.HolidaysApi.getHolidays (kHolidaysApi.java:183)
 в com.my.co.service.Праздник.HolidaysApiTest.getHolidaysTest(HolidaysApiTest.java:66) 
 в sun.reflect.NativeMethodAccessorImpl.invoke0(собственный метод) 
 в sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62) 
 в sun.reflect.Делегирование methodaccessorimpl.invoke(делегирование methodaccessorimpl.java:43) 
 в java.lang.reflect.Метод.invoke (Метод.java:498)
 в org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
 в org.junit.jupiter.engine.execution.Вызов метода.продолжить (MethodInvocation.java:60)
 в org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.продолжить (InvocationInterceptorChain.java:131) 
 в org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java: 149)
 в org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
 в org.junit.jupiter.engine.extension.Метод TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84) 
 в org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod $0(ExecutableInvoker.java:115)
 в org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke $0 (ExecutableInvoker.java:105) 
 в org.junit.jupiter.engine.execution.Цепочка вызовов InterceptorChain$InterceptedInvocation.продолжить (InvocationInterceptorChain.java:106) 
 в org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) 
 в org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) 
 в org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
 в org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) 
 в org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) 
 в org.junit.jupiter.engine.дескриптор.TestMethod TESTDESCRIPTOR.lambda$invokeTestMethod $ 6(TestMethod TESTDESCRIPTOR.java:212)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.jupiter.engine.дескриптор.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
 в org.junit.jupiter.engine.дескриптор.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137) 
 в org.junit.jupiter.engine.дескриптор.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135) 
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
 в org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) 
 в java.util.ArrayList.forEach(ArrayList.java:1257)
 в org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
 в org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) 
 в java.util.ArrayList.forEach(ArrayList.java:1257)
 в org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
 в org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) 
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
 в org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
 в org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) 
 в org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) 
 в org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute (HierarchicalTestExecutor.java:57) 
 в org.junit.platform.engine.support.hierarchical.Иерархический тестовый файл.выполнить (иерархический тестовый файл.java: 51) 
 в org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
 в org.junit.platform.launcher.core.DefaultLauncher.лямбда $выполнить $5 (DefaultLauncher.java: 211)
 в org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
 в org.junit.platform.launcher.core.DefaultLauncher.execute (DefaultLauncher.java:199)
 в org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
 в com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
 в com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) 
 в com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
 в com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
** Вызвано: ошибка = "invalid_request", error_description="{код= 415, сообщение = Неподдерживаемый тип носителя}"**
 в org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java: 119) 
 в org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33) 
 в com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4524)
 в com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3519)
 в org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType (AbstractJackson2HttpMessageConverter.java:269)
 в org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal (AbstractJackson2HttpMessageConverter.java:249)
 в org.springframework.http.converter.Абстрактныйhttpmessageconverter.read(абстрактныйhttpmessageconverter.java:199)
 в org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.HandleError (OAuth2AccessTokenSupport.java:237)
 в org.springframework.web.client.ResponseErrorHandler.HandleError (ResponseErrorHandler.java:63) 
 в org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:782)
 в org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:740)
 в org.springframework.web.client.RestTemplate.execute(RestTemplate.java:695)
 в org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken (OAuth2AccessTokenSupport.java:137)
 ... еще 75

Ниже приведена моя конфигурация для клиента Oauth2

 application.properties:
spring.security.oauth2.holiday.client.clientId=valid_key_is_here
spring.security.oauth2.holiday.client.clientSecret=valid_secret_is_here
spring.security.oauth2.holiday.client.accessTokenUri=https://dev-api.some-domain.com/auth/oauth2/v1/token
spring.security.oauth2.holiday.client.clientAuthenticationScheme=form
spring.security.oauth2.holiday.client.grantType=client_credentials
spring.security.oauth2.holiday.client.scope=read
  
 @Configuration
@EnableOAuth2Client
public class SpringOauthRestClientConfig {

    @Bean
    @ConfigurationProperties("spring.security.oauth2.holiday.client")
    public OAuth2ProtectedResourceDetails oAuthDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RestTemplate restTemplate() {
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuthDetails());

        for (HttpMessageConverter converter : restTemplate.getMessageConverters()) {
            if (converter instanceof AbstractJackson2HttpMessageConverter) {
                ObjectMapper mapper = ((AbstractJackson2HttpMessageConverter) converter).getObjectMapper();
                mapper.registerModule(new JavaTimeModule());
            }
        }
        // This allows us to read the response more than once - Necessary for debugging.
        restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory()));
        return restTemplate;
    }
}
  

Я попытался расширить класс Spring, ClientCredentialsAccessTokenProvider чтобы предоставить свою собственную реализацию obtainAccessToken() метода, чтобы я мог установить тип содержимого в заголовке. Затем я внедряю свой пользовательский класс в RestTemplate. По-прежнему возникает та же ошибка, когда Spring пытается получить токен доступа.

 public class ClientCredentialsCustomAccessTokenProvider extends ClientCredentialsAccessTokenProvider {

    @Override
    public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
        ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails)details;

        HttpHeaders headers1 = new HttpHeaders();
        headers1.add("Content-Type", "application/x-www-form-urlencoded");

        return retrieveToken(request, resource, this.getParametersForTokenRequest(resource), headers1);
    }
  

Если я использую Postman для доступа к серверу авторизации, я успешно получаю токен обратно

 {
    "tokenType": "BearerToken",
    "expiresIn": "899",
    "accessToken": "dv6fnhBALtNzlhjMyCRfa9JDYodd"
}
  

используя эти настройки в Postman

 POST request, 
Authorization - Basic with my client_id/secret as username/password, 
Headers - Content-Type = application/x-www-form-urlencoded, 
Body - grant_type = client_credentials
  

В тесте JUnit я могу установить значение токена (и обойти внедрение Spring RestTemplate ), используя ответ от Postman, и вызвать службу без проблем.

 HolidaysApi api = new HolidaysApi();
OAuth oAuth2 = (OAuth) api.getApiClient().getAuthentication("OAuth2");
oAuth2.setAccessToken("dv6fnhBALtNzlhjMyCRfa9JDYodd");
  

Ответ №1:

В моем случае Spring использует FormHttpMessageConverter класс under the covers для подготовки запроса http POST к серверу аутентификации, и он добавляется к заголовку типа содержимого «charset = UTF-8». Сервер авторизации, на который я нажимаю, не разрешает ничего, кроме ожидаемого типа содержимого (application /x-www-form-urlencoded). Может быть другой способ, но в итоге я использовал свой собственный, CustomFormHttpMessageConverter который правильно устанавливает тип содержимого в OAuth2RestTemplate компонент. Ниже я показываю вещи в обратном порядке:

По сути, скопировал FormHttpMessageConverter класс Spring и создал пользовательский класс, изменив writeMultipart() метод для установки типа содержимого без набора символов:

 public class CustomFormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
  ...
  writeMultipart(...) {
      ...
      outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    }
}
  

Затем вам нужно использовать свой пользовательский класс преобразования сообщений:

 public class CustomOAuth2AuthTokenCallback implements RequestCallback {

protected final Log logger = LogFactory.getLog(this.getClass());
private final MultiValueMap<String, String> form;
private final HttpHeaders headers;

protected CustomOAuth2AuthTokenCallback(MultiValueMap<String, String> form, HttpHeaders headers) {
    this.form = form;
    this.headers = headers;
}

public void doWithRequest(ClientHttpRequest request) throws IOException {
    request.getHeaders().putAll(this.headers);
    request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED));
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Encoding and sending form: "   this.form);
    }
    CustomFormHttpMessageConverter formHttpMessageConverter = new CustomFormHttpMessageConverter();
    formHttpMessageConverter.setCharset(null);
    formHttpMessageConverter.setMultipartCharset(null);
    formHttpMessageConverter.write(this.form, MediaType.APPLICATION_FORM_URLENCODED, request);
    }
}
  

Теперь нам нужно убедиться, что используется наш пользовательский обратный вызов:

 public class ClientCredentialsCustomAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
@Override
protected RequestCallback getRequestCallback(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) {
    return new CustomOAuth2AuthTokenCallback(form, headers);
    }
}
  

У меня было пользовательское местоположение в файле свойств для информации о клиенте, поэтому я использовал:

 @Bean
@ConfigurationProperties("spring.security.oauth2.holiday.client")
public OAuth2ProtectedResourceDetails oAuthDetails() {
    ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
    return details;
}
  

Итак, теперь у нас есть компонент нашего провайдера с обратным вызовом пользовательского токена. Все, что нам нужно на этом этапе, это указать шаблону rest, какого поставщика использовать:

 @Configuration
@EnableOAuth2Client
public class SpringOauthRestClientConfig {
@Bean
public RestTemplate restTemplate(OAuth2ProtectedResourceDetails oAuthDetails) {
    OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuthDetails);
    restTemplate.setAccessTokenProvider(new 
    ClientCredentialsCustomAccessTokenProvider());
    restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory()));
    return restTemplate;
}
  

Вместо того, чтобы пройти через все это, чтобы просто получить токен для базовой авторизации с использованием учетных данных клиента, я закончил тем, что пошел по этому маршруту (используя HttpEntity<String> ):

 String url = "https://some-domain.com/auth/oauth2/v1/token";
String credentials = "some-client-key:some-client-password";
String encodedCredentials = new String(Base64.encodeBase64(credentials.getBytes()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML, MediaType.APPLICATION_JSON));
headers.add("Authorization", "Basic "   encodedCredentials);
HttpEntity<String> request = new HttpEntity<>("grant_type=client_credentials", headers);
ResponseEntity<Oauth2Token> response = restTemplate.exchange(url, HttpMethod.POST, request, Oauth2Token.class);

boolean isSuccess = response.getStatusCode().is2xxSuccessful();
HttpHeaders respHeaders = response.getHeaders();
Oauth2Token body = response.getBody();