0

我在使用org.springframework.web.reactive.function.client.WebClientKotlin 中的 Spring Reactive WebClient () 使用 Azure AD 实现 Oauth2 时遇到问题,遵循以下文档:https ://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2- client-creds-grant-flow#first-case-access-token-request-with-a-shared-secret

我的组织有自己的租户,但问题也适用于使用https://login.microsoftonline.com/common/oauth2/v2.0/token时。

如果我执行以下 CURL,我会收到我需要参数的预期 400 错误grant_type

curl -d '' https://login.microsoftonline.com/common/oauth2/v2.0/token

这很好,这意味着我从服务中得到了一个正确的错误,我可以继续包含缺少的参数。但是,将 WebClient 用于相同的 uri 我得到 404 错误。

这是我的实现:

import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient


@Component
class AzureADClient {

    fun oidcToken(): String {
        var received = WebClient.create("https://login.microsoftonline.com/common/oauth2/v2.0/token").post()
            .retrieve()
            .bodyToMono(OidcToken::class.java)
            .block()

        return received.token
    }

    data class OidcToken(
        @JsonProperty(value = "access_token", required = true)
        val token: String,
        @JsonProperty(value = "token_type", required = true)
        val type: String,
        @JsonProperty(value = "expires_in", required = true)
        val expiresIn: Int
    )
}

这是我得到的堆栈跟踪:

org.springframework.web.reactive.function.client.WebClientResponseException$NotFound: 404 Not Found from POST https://login.microsoftonline.com/common/oauth2/v2.0/token
    at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:185)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ 404 from POST https://login.microsoftonline.com/common/oauth2/v2.0/token [DefaultWebClient]
Stack trace:
        at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:185)
        at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:209)
        at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1712)
        at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:100)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:144)
        at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onComplete(FluxContextStart.java:122)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:144)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onComplete(FluxFilterFuseable.java:165)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1713)
        at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:160)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:104)
        at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136)
        at reactor.netty.channel.FluxReceive.terminateReceiver(FluxReceive.java:421)
        at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:211)
        at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:369)
        at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:367)
        at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:416)
        at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:612)
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:90)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:102)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:321)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:295)
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1486)
        at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1235)
        at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1282)
        at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:498)
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:437)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:834)

POST在其他交互中使用带有请求的 WebClient 就好了。如果我用 a 替换 WebClient 调用,RestTemplate().postForObject()我会收到预期的 400 错误,但我正在尝试在我的应用程序中一直使用 WebClient。我尝试了类似POST的操作https://postman-echo.com/post,并且得到了合理的响应,因此它似乎特定于 login.microsoftonline.com 端点。

有没有人遇到过这个,并找到了解决方案?

4

1 回答 1

2

我找到了解决方案。首先,这篇帖子的回复表明主机不接受分块传输编码:https ://social.msdn.microsoft.com/Forums/en-US/8efc2e39-8e9c-4449-8e25-13b97a7acf15/azure -ad-oauth-20-token-endpoint-recently-started-returning-404-with-empty-response-body-but-used?forum=WindowsAzureAD

这由 curl 验证:

> curl -H "Transfer-Encoding: chunked" -d ''  https://login.microsoftonline.com/common/oauth2/v2.0/token -v
< ...
< HTTP/1.1 404 Not Found
< ...

我首先尝试扩展我的请求以包含所需的参数,如下所示:

    fun oidcToken(): String {

        val map = LinkedMultiValueMap<String, String>()

        map.add("client_id", "client_id")
        map.add("client_secret", "client_secret")
        map.add("grant_type", "grant_type")
        map.add("scope", "api://scope_id/.default")

        val received = WebClient
            .create("https://login.microsoftonline.com/common/oauth2/v2.0/token")
            .post()
            .body(BodyInserters.fromMultipartData(map))
            .retrieve()
            .bodyToMono(OidcToken::class.java)
            .block()

        return received.token
    }

这给出了与以前相同的 404 错误,因此传输也可能在这里被分块。

该解决方案原来是使用.bodyValue(map)而不是.body(BodyInserters.fromMultipartData(map)).

我尝试按照以下答案设置内容长度标头:https ://stackoverflow.com/a/51049535/9658830 。我使用这种方法确实得到了 400 错误,因此找到了服务器,但bodyValue()结果证明这是一个更简单的解决方案。

于 2020-08-04T06:24:12.067 回答