簡體   English   中英

Spring Security Oauth2 客戶端獲取訪問令牌失敗,請求代碼無效=415,消息=不支持的媒體類型

[英]Spring Security Oauth2 Client get access token fails with invalid request code=415, message=Unsupported Media Type

使用 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()方法的實現,以便我可以在標題中設置內容類型。 然后我將我的自定義類注入到 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");

在我的例子中,Spring 正在使用FormHttpMessageConverter類來准備對身份驗證服務器的 http POST 請求,並將其附加到 Content-Type 標頭“charset=UTF-8”。 我正在訪問的授權服務器除了它期望的內容類型(應用程序/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;
}

所以現在我們有了帶有自定義令牌回調的 provider 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();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM