Spring Security Oauth2 客户端获取访问令牌失败,请求代码无效=415,消息=不支持的媒体类型
Posted
技术标签:
【中文标题】Spring Security Oauth2 客户端获取访问令牌失败,请求代码无效=415,消息=不支持的媒体类型【英文标题】:Spring Security Oauth2 Client get access token fails with invalid request code=415, message=Unsupported Media Type 【发布时间】:2020-12-30 19:11:43 【问题描述】:使用 Spring Boot,我在配置类中设置了一个 Oauth2RestTemplate
bean,并在属性文件中设置了适当的属性。我使用 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=[val from props], client_secret=[val from props] error="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.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148) 在 org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(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.holiday.HolidaysApiTest.getHolidaysTest(HolidaysApiTest.java:66) 在 sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 在 sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在 sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 在 java.lang.reflect.Method.invoke(Method.java:498) 在 org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686) 在 org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) 在 org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(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.InvocationInterceptorChain$InterceptedInvocation.proceed(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.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212) 在 org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) 在 org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208) 在 org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137) 在 org.junit.jupiter.engine.descriptor.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.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) 在 org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248) 在 org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$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) **引起:error="invalid_request", error_description="code=415, message=Unsupported Media Type"** 在 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.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.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()
方法实现,以便我可以在标题中设置 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"
在 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 测试中,我可以使用 Postman 的响应设置令牌值(并绕过 RestTemplate
的 Spring 注入)并毫无问题地调用服务。
HolidaysApi api = new HolidaysApi();
OAuth oAuth2 = (OAuth) api.getApiClient().getAuthentication("OAuth2");
oAuth2.setAccessToken("dv6fnhBALtNzlhjMyCRfa9JDYodd");
【问题讨论】:
【参考方案1】:在我的例子中,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, 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;
所以现在我们有了带有自定义令牌回调的提供程序 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();
【讨论】:
以上是关于Spring Security Oauth2 客户端获取访问令牌失败,请求代码无效=415,消息=不支持的媒体类型的主要内容,如果未能解决你的问题,请参考以下文章
带有 spring-security 的 OAuth2 - 通过 HTTP 方法限制 REST 访问
在 Spring Security 中具有密码授权的 oAuth2 客户端
spring security + oauth2 + reactjs + restful http客户端
Spring Security OAuth2 Demo —— 客户端模式(ClientCredentials)