问题描述
使用Spring Boot,我在配置类中设置了一个oauth2resttemplate
bean,并在属性文件中设置了适当的属性。我已经使用Swagger代码生成了客户端存根。当我尝试调用RESTful API时,Spring无法获取访问令牌,其根本原因是“不受支持的媒体类型”。以下是堆栈跟踪,我的Spring Security客户端配置和修复尝试。任何帮助将不胜感激!
Retrieving token from https://dev-api.some-domain.com/auth/oauth2/v1/token ClientCredentialsAccesstokenProvider.doWithRequest - Encoding and sending form: {grant_type=[client_credentials],scope=[read],client_id=[val from props],client_secret=[val from props]} error="access_denied",error_description="Access token denied. at org.springframework.security.oauth2.client.token.OAuth2AccesstokenSupport.retrievetoken(OAuth2AccesstokenSupport.java:142) at org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccesstokenProvider.obtainAccesstoken(ClientCredentialsAccesstokenProvider.java:44) at org.springframework.security.oauth2.client.token.AccesstokenProviderChain.obtainNewAccesstokenInternal(AccesstokenProviderChain.java:148) at org.springframework.security.oauth2.client.token.AccesstokenProviderChain.obtainAccesstoken(AccesstokenProviderChain.java:121) at org.springframework.security.oauth2.client.oauth2resttemplate.acquireAccesstoken(oauth2resttemplate.java:221) at org.springframework.security.oauth2.client.oauth2resttemplate.getAccesstoken(oauth2resttemplate.java:173) at org.springframework.security.oauth2.client.oauth2resttemplate.createRequest(oauth2resttemplate.java:105) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735) at org.springframework.security.oauth2.client.oauth2resttemplate.doExecute(oauth2resttemplate.java:128) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:651) at com.my.co.service.holidays.client.invoker.apiclient.invokeAPI(apiclient.java:518) at com.my.co.service.holidays.client.api.HolidaysApi.getHolidays(kHolidaysApi.java:183) at com.my.co.service.holiday.HolidaysApiTest.getHolidaysTest(HolidaysApiTest.java:66) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84) at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at java.util.ArrayList.forEach(ArrayList.java:1257) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at java.util.ArrayList.forEach(ArrayList.java:1257) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248) at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211) at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132) at com.intellij.junit5.junit5IdeaTestRunner.startRunnerWithArgs(junit5IdeaTestRunner.java:69) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) **Caused by: error="invalid_request",error_description="{code=415,message=Unsupported Media Type}"** at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:119) at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4524) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3519) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:269) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:249) at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:199) at org.springframework.security.oauth2.client.token.OAuth2AccesstokenSupport$AccesstokenErrorHandler.handleError(OAuth2AccesstokenSupport.java:237) at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:782) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:740) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:695) at org.springframework.security.oauth2.client.token.OAuth2AccesstokenSupport.retrievetoken(OAuth2AccesstokenSupport.java:137) ... 75 more
以下是我对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()
方法的实现,因此可以在标头中设置Content-Type。然后,将自定义类注入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"
}
在邮递员中使用这些设置
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测试中,我可以使用Postman的响应来设置令牌值(并绕过RestTemplate
的Spring注入),并毫无问题地调用服务。
HolidaysApi api = new HolidaysApi();
OAuth oAuth2 = (OAuth) api.getapiclient().getAuthentication("OAuth2");
oAuth2.setAccesstoken("dv6fnhBALtNzlhjMyCRfa9JDYodd");
解决方法
在我的情况下,Spring使用幕后的FormHttpMessageConverter
类准备对身份验证服务器的http POST请求,并将其追加到Content-Type头“ charset = UTF-8”。我要访问的授权服务器不允许使用除期望的Content-Type(应用程序/ x-www-form-urlencoded)之外的任何内容。可能还有另一种方法,但是我最终使用了自己的CustomFormHttpMessageConverter
,它可以将Content-Type正确地设置到OAuth2RestTemplate
bean中。下面,我以相反的顺序显示事物:
实质上复制了Spring FormHttpMessageConverter
类并创建了一个自定义类,更改了writeMultipart()
方法以设置不带字符集的Content-Type:
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,HttpHeaders headers) {
return new CustomOAuth2AuthTokenCallback(form,headers);
}
}
我在属性文件中有一个用于客户信息的自定义位置,所以我使用了:
@Bean
@ConfigurationProperties("spring.security.oauth2.holiday.client")
public OAuth2ProtectedResourceDetails oAuthDetails() {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
return details;
}
所以现在我们有了带有自定义令牌回调的提供程序bean。此时,我们所需要做的就是告诉其余模板使用哪个提供程序:
@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();