Spring WebClient POST到login.microsoftonline.com给出404错误



我在Kotlin中使用Spring Reactive WebClient(org.springframework.web.reactive.function.client.WebClient(使用Azure AD实现Oauth2时遇到问题,请参阅以下文档:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#first-案例访问-带共享机密的问题

我的组织有自己的租户,但在使用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)

我在其他交互中使用WebClient和POST请求也很好。如果我用RestTemplate().postForObject()替换WebClient调用,我会得到预期的400错误,但我正在尝试在应用程序中一直使用WebClient。我试着做一个类似的POSThttps://postman-echo.com/post,得到了合理的回应,所以它似乎是特定于login.microsoftonline.com的终点。

有人遇到过这种情况,并找到了解决方案吗?

我找到了一个解决方案。首先,这篇文章的回复表明主机不接受分块传输编码: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()是一个更容易的解决方案。

相关内容

最新更新