【问题标题】:failing to add client credentials (clientid/clientsecret) at Spring Webclient: Request processing failed ... 401 UNAUTHORIZED未能在 Spring Webclient 上添加客户端凭据(clientid/clientsecret):请求处理失败 ... 401 UNAUTHORIZED
【发布时间】:2021-11-17 06:19:10
【问题描述】:

我正在尝试使用 WebClient 来使用提供令牌的端点。 使用 Postman 它可以按预期工作。从邮递员导出的卷曲是:

curl --location --request POST 'https://mycomp.url/api/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=xxx' \
--data-urlencode 'client_secret=yyy' \
--data-urlencode 'grant_type=client_credentials'

我正在根据上面相同的 curl 配置 webclient 调用。

这是我的 WebClient 配置:

@Configuration
class ClientConfiguration {

    @Bean
    fun webClient(): WebClient = WebClient.builder()
        .clientConnector(
            ReactorClientHttpConnector(
                HttpClient.from(
                    TcpClient
            .create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
            .doOnConnected { connection: Connection ->
                connection.addHandlerLast(ReadTimeoutHandler(10000, TimeUnit.MILLISECONDS))
                connection.addHandlerLast(WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))
            }))
        )
        .build()
}

这是用于接收令牌的 webclient 帖子:

@Service
class TokenService(private val webClient: WebClient) {

    fun postAsynchronous(): Mono<TokenResponse> = webClient
        .post()
        .uri(UriComponentsBuilder
            .fromHttpUrl("https://mycomp.url")
            .path("/api/oauth/token")
            .build()
            .toUri())
        .header("grant_type","client_credentials")
        .header("client_id","xxx")
        .header("client_secret","yyy")
        .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") 
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
        .onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
        .bodyToMono(TokenResponse::class.java)
}

这是我的 build.gradle.kts(相关部分):

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.4.10"
    id("org.jetbrains.kotlin.kapt") version "1.4.10"
    kotlin("plugin.spring") version "1.5.20"
    id("org.springframework.boot") version "2.4.7"
    //kotlin("jvm") version "1.5.30"

    id("io.spring.dependency-management") version "1.0.10.RELEASE"

}

val kotlinVersion: String by project
val springVersion: String by project
val projectGroupId: String by project
val projectVersion: String by project

group = projectGroupId
version = projectVersion

repositories {
    mavenLocal()
    ... some internal artifactories
    mavenCentral()
}

// add dependencies
dependencies {
    kapt(kotlin("stdlib", kotlinVersion))
    implementation(kotlin("stdlib-jdk8"))
    implementation(kotlin("reflect", kotlinVersion))

    implementation("org.springframework.boot:spring-boot-dependencies:2.4.7")
    implementation("org.springframework.boot:spring-boot-starter:2.4.7")
    implementation("org.springframework.boot:spring-boot-starter-web:2.4.7")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.0.3")
    implementation("io.github.openfeign:feign-okhttp:10.2.0")

    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.2")
}

整个例外是:

2021/09/23 17:33:53.123 [http-nio-8080-exec-2] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
2021/09/23 17:33:53.123 [http-nio-8080-exec-2] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
2021/09/23 17:33:53.124 [http-nio-8080-exec-2] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
2021/09/23 17:33:54.396 [http-nio-8080-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED] with root cause
java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED
    at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ 401 from POST https://mycomp-url/api/oauth/token [DefaultWebClient]
Stack trace:
        at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
        at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:15)
        at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec$StatusHandler.apply(DefaultWebClient.java:693)
        at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.applyStatusHandlers(DefaultWebClient.java:652)
        at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.handleBodyMono(DefaultWebClient.java:621)
        at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.lambda$bodyToMono$2(DefaultWebClient.java:541)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125)

我还尝试了其他方法以防万一。

我保持 webclient 不变,只是更改发送凭据的方式。

首先我创建了一个包含所有三个参数的简单类:

data class TokenRequest(
    var grantType: String,
    var clientId: String,
    var clientSecret: String
)

然后我将 webclient.post 修改为

fun postAsynchronous(): Mono<TokenResponse> = webClient
    .post()
    .uri(UriComponentsBuilder
        .fromHttpUrl("https://mycomp-url")
        .path("/api/oauth/token")
        .build()
        .toUri())
    .body(BodyInserters.fromValue(TokenRequest("client_credentials","xxx", "yyy")))
    .header(HttpHeaders.CONTENT_TYPE, "application/json")
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
    .onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
    .bodyToMono(TokenResponse::class.java)

我遇到了完全相同的问题:

2021/09/23 18:01:55.994 [http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED] with root cause
java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED
    at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ 401 from POST https://mycomp.url/api/oauth/token [DefaultWebClient]
Stack trace:
        at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
        at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:15)
        at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec$StatusHandler.apply(DefaultWebClient.java:693)

*** 2021 年 10 月 7 日编辑

通过 Aniket Singla 提案,我遇到了这个新问题:

[reactor-tcp-nio-2] WARN  r.n.http.client.HttpClientConnect - [id:9270e5dc-1, L:/10.92.12.165:58268 - R:mycomp-url/x.x.x.x:443] The connection observed an error
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest
    at org.springframework.web.reactive.function.BodyInserters.unsupportedError(BodyInserters.java:391)
    ...
[http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.reactive.function.client.WebClientRequestException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest; nested exception is org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest] with root cause
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest

有了 Maciej Dobrowolski 的提议,我得到了这个新的例外:

2021/10/07 17:36:29.098 [http-nio-8080-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type
 at [Source: (io.netty.buffer.ByteBufInputStream); line: 8, column: 1] (through reference chain: com.mycomp.application.models.token.TokenResponse["result"])] with root cause
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type
 at [Source: (io.netty.buffer.ByteBufInputStream); line: 8, column: 1] (through reference chain: com.mycomp.application.models.token.TokenResponse["result"])
    at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:112)

***已编辑

data class TokenResponse (
    val result: String
)

【问题讨论】:

    标签: spring spring-boot postman spring-webflux spring-webclient


    【解决方案1】:

    使用--data-urlencode curl 选项,您正在向请求的正文添加一个参数。在您的 Kotlin 代码中,您不是在请求的正文中传递相同的数据,而是在标头中传递。

    您应该做的(模仿邮递员的行为)是使用BodyInserters 在请求正文中传递grant_typeclient_idclient_secret,如下所示:

    webClient
            .post()
            .uri(UriComponentsBuilder
                .fromHttpUrl("https://mycomp.url")
                .path("/api/oauth/token")
                .build()
                .toUri())
            .body(BodyInserters.fromFormData("grant_type", "client_credentials")
                               .with("client_id", "xxx")
                               .with("client_secret", "yyy")) 
            .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") 
            .retrieve()
            // ...
            
    

    【讨论】:

    • Maciej,谢谢。我得到的答案是空的,但我刚刚仔细检查了一遍,我使用的是完全相同的 client_id 和 client_secret,我通过邮递员成功使用了。我相信它正在调用端点,但我看不到任何空答案的原因(我添加了有关原始问题的更多详细信息,但简而言之,“根本原因 com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException:实例化[简单类型,类 ...TokenResponse] JSON 属性结果的值失败,因为创建者参数结果的值缺失(因此为 NULL),该值是不可为空的)
    • 您能否也将 TokenResponse 类定义添加到问题中?
    • 谢谢,我在上面做了。
    • 这不是最好的建议,但考虑到这里是晚上 11 点,也不是最差的。我猜您在 TokenResponse 中将 val 更改为 var 以摆脱此异常
    • @JimC 看看这个问题 cmets,也许对你有帮助:github.com/FasterXML/jackson-module-kotlin/issues/87
    【解决方案2】:

    在 headers 中提供 url 编码数据将不起作用,您只需在 headers 中告知您将使用“application/x-www-form-urlencoded”作为内容类型,否则 webclient 将负责转换正文转换为 url 编码形式。对您的 postAsynchronous 方法进行了一些更改,应该可以解决您的问题。

    fun postAsynchronous(): Mono<TokenResponse> = webClient
        .post()
        .uri(UriComponentsBuilder
            .fromHttpUrl("https://des-sts-int.mbi.cloud.ihf")
            .path("/api/oauth/token")
            .build()
            .toUri())
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
        .body(BodyInserters.fromFormData("grant_type", "client_credentials")
                               .with("client_id", "xxx")
                               .with("client_secret", "yyy")) )
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
        .onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
        .bodyToMono(TokenResponse::class.java)
    

    【讨论】:

    • 谢谢。现在我得到一个新的例外:bodyType=com.mycomp.application.models.token.TokenRequest 不支持内容类型'application/x-www-form-urlencoded'(我将在我的问题中添加有关您的解决方案的更多详细信息) .
    猜你喜欢
    • 1970-01-01
    • 2021-08-25
    • 1970-01-01
    • 2021-05-29
    • 1970-01-01
    • 2021-12-16
    • 2020-02-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多