#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();