有时需要允许不安全的 HTTPS 连接,例如在一些应该适用于任何网站的网络爬行应用程序中。我使用了一个这样的解决方案与旧的 HttpsURLConnection API,该 API 最近被 JDK 11 中的新的 HttpClient API 取代。使用这个新 API 允许不安全的 HTTPS 连接(自签名或过期的证书)的方法是什么?
UPD:我尝试过的代码(在 Kotlin 中,但直接映射到 Java):
val trustAllCerts = arrayOf<TrustManager>(object: X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
})
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())
val sslParams = SSLParameters()
// This should prevent host validation
sslParams.endpointIdentificationAlgorithm = ""
httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.sslParameters(sslParams)
.build()
但是在发送时我有例外(尝试使用自签名证书在本地主机上):
java.io.IOException: No name matching localhost found
使用 IP 地址而不是本地主机会出现“不存在主题备用名称”异常。
对 JDK 进行一些调试后,我发现
sslParams
在引发异常的地方确实被忽略了,并且使用了一些本地创建的实例。进一步的调试表明,影响主机名验证算法的唯一方法是将 jdk.internal.httpclient.disableHostnameVerification
系统属性设置为 true。这似乎是一个解决方案。上面代码中的SSLParameters
没有任何作用,所以这部分可以丢弃。使其仅可全局配置看起来像是新 HttpClient API 中的严重设计缺陷。
正如已经建议的,您需要一个 SSLContext 来忽略错误的证书。在问题的链接之一中获取 SSLContext 的确切代码应该通过基本上创建一个不查看证书的空 TrustManager 来工作:
private static TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
public static void main (String[] args) throws Exception {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new SecureRandom());
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
上述问题显然是所有站点都完全禁用了服务器身份验证。如果只有一个错误的证书,那么您可以使用以下命令将其导入密钥库:
keytool -importcert -keystore keystorename -storepass pass -alias cert -file certfile
然后使用读取密钥库的 InputStream 初始化 SSLContext,如下所示:
char[] passphrase = ..
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(i, passphrase); // i is an InputStream reading the keystore
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, passphrase);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
上述任一解决方案都适用于自签名证书。第三种选择是在服务器提供有效的非自签名证书的情况下,但对于与其提供的证书中的任何名称都不匹配的主机,则系统属性“jdk.internal.httpclient.disableHostnameVerification”可以设置为“true”,这将强制以与之前使用 HostnameVerifier API 相同的方式接受证书。请注意,在正常部署中,预计不会使用任何这些机制,因为应该可以自动验证任何正确配置的 HTTPS 服务器提供的证书。
使用 Java 11,您也可以做类似的工作 如共享链接中所选答案中所述,其中
HttpClient
构建为:
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(<timeoutInSeconds> * 1000))
.sslContext(sc) // SSL context 'sc' initialised as earlier
.sslParameters(parameters) // ssl parameters if overriden
.build();
有样品要求
HttpRequest requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("https://www.example.com/getSomething"))
.GET()
.build();
可以执行为:
httpClient.send(requestBuilder, HttpResponse.BodyHandlers.ofString()); // sends the request
更新评论,要禁用主机名验证,目前可以使用系统属性:
-Djdk.internal.httpclient.disableHostnameVerification
可以以编程方式设置,如下所示:-
final Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
提供不进行证书验证的 X509TrustManager(如上面的答案)不足以禁用主机名验证,因为 SSLContext 的实现会使用 X509ExtendedTrustManager“包装”提供的 X509TrustManager(如果它不是 X509ExtendedTrustManager)。包装“扩展”X509ExtendedTrustManager 执行主机名验证。
因此,避免主机名验证的一种方法是提供不进行主机名验证的 X509ExtendedTrustManager。这可以按如下方式完成:
import java.net.Socket;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;
SSLContext context = SSLContext.getInstance("TLS");
context.init(
null,
new TrustManager[]
{
new X509ExtendedTrustManager()
{
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type)
{
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final Socket a_socket)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final Socket a_socket)
{
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final SSLEngine a_engine)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final SSLEngine a_engine)
{
}
}
},
null);
以通常的方式将此 SSLContext 提供给 HTTPClient.Builder(如前面的答案所示)。
此方法的优点是可以在每个 HttpClient 基础上应用(即不是在 JVM 范围内)。
有关工作参考,您可以查看我的要点:https://gist.github.com/wildone/d3aee880d1ffe4ca8b7378665f9c31f2