如何在Spring WebClient中禁用主机名验证?

11

我正在使用Spring Webflux WebClient工具调用API。该API服务器地址为HTTPS,是一个没有域名的IP地址。 我需要在WebClient中禁用主机名验证。现在的异常如下:

Caused by: java.security.cert.CertificateException: No subject alternative names matching IP address 180.101.147.89 found
    at sun.security.util.HostnameChecker.matchIP(HostnameChecker.java:168) ~[na:1.8.0_211]
    at sun.security.util.HostnameChecker.match(HostnameChecker.java:94) ~[na:1.8.0_211]
    at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:461) ~[na:1.8.0_211]
    at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:442) ~[na:1.8.0_211]
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:260) ~[na:1.8.0_211]
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144) ~[na:1.8.0_211]
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1626) ~[na:1.8.0_211]
    ... 28 common frames omitted
@Bean
    public WebClient telcomWebclient(WebClient.Builder webClientBuilder,
                                     @Value("${telcom.api.host}") String telcomApiHost,
                                     @Value("${telcom.api.certificate-name}") String telcomApiCertificateName,
                                     @Value("${telcom.api.certificate-store-pass}") String telcomApiCertificateStorePass) {
        try {
            KeyStore selfCert = KeyStore.getInstance("pkcs12");
            selfCert.load(getClass().getResourceAsStream("/cert/outgoing.CertwithKey.pkcs12"), "IoM@1234".toCharArray());
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509");
            kmf.init(selfCert, "IoM@1234".toCharArray());

            KeyStore caCert = KeyStore.getInstance("jks");
            caCert.load(getClass().getResourceAsStream("/cert/" + telcomApiCertificateName), telcomApiCertificateStorePass.toCharArray());
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509");
            tmf.init(caCert);

            SslContext sslContext = SslContextBuilder.forClient()
                    .keyManager(kmf)
                    .trustManager(tmf)
                    .build();
            HttpClient httpClient = HttpClient.create().create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
            ClientHttpConnector clientHttpConnector = new ReactorClientHttpConnector(httpClient);
            return webClientBuilder.clientConnector(clientHttpConnector).baseUrl(telcomApiHost).build();
        } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
            log.error("Config webclient,error occurs", e);
            System.exit(-1);
        }
        return null;
    }
2个回答

12

除了完全禁用SSL验证(我不建议这样做),还可以通过像这样传递InsecureTrustManagerFactory.INSTANCE来禁用:

SslContext sslContext = SslContextBuilder.forClient()
                    .keyManager(kmf)
                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
                    .build();
你可以通过设置一个自定义的SNIMatcher来配置HttpClient,从而实现基本上覆盖主机名验证的功能,具体操作如下: SNIMatcher
HttpClient.create().create().secure(sslContextSpec -> sslContextSpec
    .sslContext(sslContext)
    .handlerConfigurator(sslHandler -> 
        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);
);
我已经测试过并验证了它的可行性。希望这可以帮到你! 这受到此处答案的启发:使用反应堆Netty为spring-webflux WebClient配置主机名验证程序

1
太好了!非常感谢! - wangyongjun
@James,它可以在webclient中为所有人正常工作并绕过主机名验证。如果我们只想为几个主机绕过主机名验证,而其余主机仍需进行验证,我们是否有任何解决方案?类似于在调用rest模板时添加检查的X509Hostnameverifier,以确定我们要绕过哪个域。 - parag mangal

10

实际上,SNIMatcher只用于服务器端。这可以通过在matches()方法的返回处设置断点来验证。执行永远不会停止在该断点处。
实际上跳过主机名验证的原因是SSLParameters被覆盖时,新的params.identificationAlgorithm值为null而不是"HTTPS"。
这里有一段更简单的代码,其效果与上面的代码一样。

           HttpClient.create().secure(sslContextSpec -> sslContextSpec
                   .sslContext(sslContext)
                   .handlerConfigurator(sslHandler -> {
                       SSLEngine engine = handler.engine();
                       SSLParameters params = new SSLParameters(engine.getSSLParameters().getCipherSuites(), engine.getSSLParameters().getProtocols());
                       // With java update null value is no longer sufficient to skip host name verification
                       params.setEndpointIdentificationAlgorithm("");
                       engine.setSSLParameters(params);
                   }));

可以用,谢谢!稍微简化一下。SSLEngine engine = sslHandler.engine(); SSLParameters params = engine.getSSLParameters(); // 随着Java更新,null值不再足以跳过主机名验证 params.setEndpointIdentificationAlgorithm(""); engine.setSSLParameters(params); - coldkreap
1
Lambda参数是sslHandler,但块中的参数是handler,只是提醒一下。 - coldkreap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接