Spring Webflux:第二个请求上的双向认证失败

huangapple go评论62阅读模式
英文:

Spring Webflux : mutual authentication fails on second request

问题

我处于TLS双向认证的上下文中(服务器证书和客户端证书)。

我的客户端是一个使用Webflux(WebClient)的Spring Boot应用程序。
WebClient实例只创建一次并一直运行(客户端作为24/24小时的服务运行)。

我的服务器也是一个配置了双向认证的Spring Boot应用程序。

第一个请求运行得很顺利。

第二个请求(大约10分钟后)总是失败,客户端报错如下:

javax.net.ssl.SSLException: Received fatal alert: internal_error
        at sun.security.ssl.Alert.createSSLException(Alert.java:133) ~[?:?]
        ... (省略部分错误信息)

... 服务器端也报错如下:

org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted

我猜想服务器期望客户端重新进行身份验证,而客户端可能不觉得有必要再次进行身份验证。

我是否需要进一步配置我的WebClient以保持连接开放或类似的操作?

我可以为每个请求重新创建WebClient,但我认为在正确配置的情况下这是不必要的。

感谢您的帮助。

英文:

I am in a context of TLS mutual authentication (server certificate and client certificate).

My client is a Spring Boot application with webflux (WebClient).
The WebClient instance is created once and for all (the client runs as a service 24/24h).

My server is also a Spring Boot application configured for mutual authentication.

The first request works like a charm.

The second request (about 10 minutes later) always fails with the following error on client side :

javax.net.ssl.SSLException: Received fatal alert: internal_error
        at sun.security.ssl.Alert.createSSLException(Alert.java:133) ~[?:?]
        at sun.security.ssl.Alert.createSSLException(Alert.java:117) ~[?:?]
        at sun.security.ssl.TransportContext.fatal(TransportContext.java:356) ~[?:?]
        at sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:293) ~[?:?]
        at sun.security.ssl.TransportContext.dispatch(TransportContext.java:202) ~[?:?]
        at sun.security.ssl.SSLTransport.decode(SSLTransport.java:171) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:736) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:691) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:506) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:482) ~[?:?]
        at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:679) ~[?:?]
        at io.netty.handler.ssl.SslHandler$SslEngineType$3.unwrap(SslHandler.java:297) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1352) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1245) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1294) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:499) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:397) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at java.lang.Thread.run(Thread.java:831) [?:?]

... and the following error on server side :

org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted

I suppose the server expects the client to authenticate again and I suppose the client does not feel the need to authenticate again.

Do I need to configure further my WebClient in order to maintain the connection open or something like that ?

I could recreate the WebClient for every request but I think it's not necessar with the proper configuration.

Thanks for helping.

答案1

得分: 0

我最终解决了这个问题。

这是WebClient实例化的有问题的代码:

var keyStoreInputStream = keyStoreResource.getInputStream();
var keyStore = readKeyStore(keyStoreType, keyStoreInputStream, keyStorePassword);
var keyManagerFactory = KeyManagerFactory.getInstance(getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);

var trustStoreInputStream = trustStoreResource.getInputStream();
var trustStore = readKeyStore(trustStoreType, trustStoreInputStream, trustStorePassword);
var trustManagerFactory = TrustManagerFactory.getInstance(getDefaultAlgorithm());
trustManagerFactory.init(trustStore);

var sslContext = forClient().
        keyManager(keyManagerFactory).
        trustManager(trustManagerFactory).
        build();

var httpClient = HttpClient.create().
        secure(ssl -> ssl.sslContext(sslContext));

var clientConnector = new ReactorClientHttpConnector(httpClient);

var webClient = WebClient.builder().
        clientConnector(clientConnector).
        baseUrl(baseUrl).
        build();

使用这个配置后,在60秒的不活动(无请求)后,底层通道会关闭。我不知道是客户端还是服务器配置的原因。

一旦通道关闭,如果我发出另一个请求,它会自动打开一个新的通道,但会重用相同的SSL会话。这就是为什么客户端不执行新的SSL握手的原因。这就是我在服务器端收到以下错误的原因:
org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted

我解决这个问题的想法是在通道关闭之前让SSL会话过期,通过以下附加的SSL上下文配置:

var sslContext = forClient().
        keyManager(keyManagerFactory).
        trustManager(trustManagerFactory).
        sessionTimeout(30).
        build();

它确实起作用,SSL会话在通道之前超时,如果需要的话会再次执行SSL握手。

我认为这不是一个非常干净的解决方案,我仍然愿意听取建议,但我认为这个问题已经解决了。

英文:

I ended up fixing this issue.

This was the faulty code for WebClient instanciation:

var keyStoreInputStream = keyStoreResource.getInputStream();
var keyStore = readKeyStore(keyStoreType, keyStoreInputStream, keyStorePassword);
var keyManagerFactory = KeyManagerFactory.getInstance(getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);

var trustStoreInputStream = trustStoreResource.getInputStream();
var trustStore = readKeyStore(trustStoreType, trustStoreInputStream, trustStorePassword);
var trustManagerFactory = TrustManagerFactory.getInstance(getDefaultAlgorithm());
trustManagerFactory.init(trustStore);

var sslContext = forClient().
		keyManager(keyManagerFactory).
		trustManager(trustManagerFactory).
		build();

var httpClient = HttpClient.create().
		secure(ssl -> ssl.sslContext(sslContext));

var clientConnector = new ReactorClientHttpConnector(httpClient);

var webClient = WebClient.builder().
		clientConnector(clientConnector).
		baseUrl(baseUrl).
		build();

With this configuration, the underlying channel is closed after 60 seconds of inactivity (without requests). I don't know if it's due to the client or server configuration.

Once the channel is closed, if I do another request, it automatically opens a new channel but it reuses the same SSL session. That's why the client does not perform a new SSL handshake. That's why I got the following error on server side:

org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted

My idea to fix this issue was to make the SSL session expire BEFORE the channel does with this additional SSL context configuration:

var sslContext = forClient().
		keyManager(keyManagerFactory).
		trustManager(trustManagerFactory).
		sessionTimeout(30).
		build();

It happens to work, the SSL session times out before the channel does and the SSL handshake is performed again if necessary.

I think it's not a very clean solution and I'm still open to suggestions but I consider this issue as fixed.

huangapple
  • 本文由 发表于 2023年6月15日 23:44:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/76483376.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定