使用 reactor netty 为 spring-webflux WebClient 配置 HostnameVerifier
Posted
技术标签:
【中文标题】使用 reactor netty 为 spring-webflux WebClient 配置 HostnameVerifier【英文标题】:Configure HostnameVerifier with reactor netty for spring-webflux WebClient 【发布时间】:2019-01-03 15:22:24 【问题描述】:我正在尝试使用 ssl 和客户端主机名验证配置 spring-webflux WebClient(在引擎盖下使用反应器 netty)。为我提供了 javax.net.ssl.SSLContext、HostnameVerifier 和可信主机名列表(作为字符串列表)。
到目前为止,我已经使用 SSLContext 配置了 WebClient,但我找不到配置主机名验证的方法。
陈述我的问题:我有一组受信任的服务主机名(字符串列表)和一个 HostnameVerifier。我想用它来配置我的 WebClient。
有没有可能用 javax.net.ssl.HostnameVerifier 做到这一点?在 reactor netty 中是否有替代方法?
这是我目前得到的:
WebClient.builder()
.clientConnector(
new ReactorClientHttpConnector(
opt -> opt.sslContext(new JdkSslContext(mySSLContext,
true, ClientAuth.OPTIONAL))))
.build();
【问题讨论】:
【参考方案1】:您应该提供有效的证书颁发机构证书 (trustManager()
) 和可选的用户证书以及用于授权的私钥和私钥密码 (keyManager()
)。
您的服务 SSL 证书应由您在 trustManager() 中定义的同一 CA 签名。
正在使用服务主机名自动验证主机名。如果不匹配 java.security.cert.CertificateException: No subject alternative names present
将抛出异常。实际上我找不到省略主机名验证的方法(不使用 .trustManager(InsecureTrustManagerFactory.INSTANCE)
省略整个 SSL 证书验证)。
我已经在本地测试了这个解决方案。我的网络服务在我的本地机器上运行,但它的 SSL 证书只包含 DNS 名称,而不是 IP 地址。 因此,出于调试目的,我在 hosts 文件中添加了条目,并将我的服务 IP 映射到正确的 DNS 名称。
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(new FileInputStream(caPath))
.keyManager(
new FileInputStream(userCertPath),
new FileInputStream(userPrivateKeyPath),
userPrivateKeyPassword
)
.build();
HttpClient httpClient = HttpClient.create()
.secure(t -> t.sslContext(sslContext));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
【讨论】:
【参考方案2】:我已尝试使用 Netty HttpClient 进行以下解决方案,并且效果很好(通过使用自定义主机名匹配器禁用主机名验证)
public HttpClient getHttpClient(HttpClientProperties properties)
// configure pool resources
HttpClientProperties.Pool pool = properties.getPool();
ConnectionProvider connectionProvider;
if (pool.getType() == DISABLED)
connectionProvider = ConnectionProvider.newConnection();
else if (pool.getType() == FIXED)
connectionProvider = ConnectionProvider.fixed(pool.getName(),
pool.getMaxConnections(), pool.getAcquireTimeout());
else
connectionProvider = ConnectionProvider.elastic(pool.getName());
HttpClient httpClient = HttpClient.create(connectionProvider)
.tcpConfiguration(tcpClient ->
if (properties.getConnectTimeout() != null)
tcpClient = tcpClient.option(
ChannelOption.CONNECT_TIMEOUT_MILLIS,
properties.getConnectTimeout());
// configure proxy if proxy host is set.
HttpClientProperties.Proxy proxy = properties.getProxy();
if (StringUtils.hasText(proxy.getHost()))
tcpClient = tcpClient.proxy(proxySpec ->
ProxyProvider.Builder builder = proxySpec
.type(ProxyProvider.Proxy.HTTP)
.host(proxy.getHost());
PropertyMapper map = PropertyMapper.get();
map.from(proxy::getPort).whenNonNull().to(builder::port);
map.from(proxy::getUsername).whenHasText()
.to(builder::username);
map.from(proxy::getPassword).whenHasText()
.to(password -> builder.password(s -> password));
map.from(proxy::getNonProxyHostsPattern).whenHasText()
.to(builder::nonProxyHosts);
);
return tcpClient;
);
HttpClientProperties.Ssl ssl = properties.getSsl();
if (ssl.getTrustedX509CertificatesForTrustManager().length > 0
|| ssl.isUseInsecureTrustManager())
httpClient = httpClient.secure(sslContextSpec ->
// configure ssl
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
X509Certificate[] trustedX509Certificates = ssl
.getTrustedX509CertificatesForTrustManager();
if (trustedX509Certificates.length > 0)
sslContextBuilder.trustManager(trustedX509Certificates);
else if (ssl.isUseInsecureTrustManager())
sslContextBuilder
.trustManager(InsecureTrustManagerFactory.INSTANCE);
sslContextSpec.sslContext(sslContextBuilder)
.defaultConfiguration(ssl.getDefaultConfigurationType())
.handshakeTimeout(ssl.getHandshakeTimeout())
.closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout())
.closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout())
.handlerConfigurator(
(handler)->
SSLEngine engine = handler.engine();
//engine.setNeedClientAuth(true);
SSLParameters params = new SSLParameters();
List<SNIMatcher> matchers = new LinkedList<>();
SNIMatcher matcher = new SNIMatcher(0)
@Override
public boolean matches(SNIServerName serverName)
return true;
;
matchers.add(matcher);
params.setSNIMatchers(matchers);
engine.setSSLParameters(params);
)
;
);
return httpClient;
它使用 nettys handlerConfigurator 来配置 SSLEngine 并将其与自定义匹配器一起使用
【讨论】:
我发现此答案具有误导性,因为它提到了自定义 SNI(服务器名称指示)的解决方案,而问题似乎与自定义 SAN(主题备用名称)验证更相关(即证书中的服务器名称与真实名称相比)使用的主机名)。以上是关于使用 reactor netty 为 spring-webflux WebClient 配置 HostnameVerifier的主要内容,如果未能解决你的问题,请参考以下文章
使用 Reactor Netty 配置 Spring Boot 以监听 2 个端口
在没有 servlet api 的 webflux 项目中使用 OAuth2 和 Spring Security OAuth2 和 reactor netty