我在网络抓取工具中使用Apache HttpClient,该抓取工具仅用于抓取公共数据。
我希望它能够对具有无效证书的网站进行爬网,无论其有效性如何。
我的搜寻器不会传递任何用户名,密码等,也不会发送或接收任何敏感数据。
对于此用例,如果存在网站的http
版本,我会对其进行爬网,但有时当然不行。
如何使用 Apache的HttpClient 完成此操作?
我尝试了一些建议,例如this one,但是对于某些无效的证书,它们仍然失败,例如:
failed for url:https://dh480.badssl.com/, reason:java.lang.RuntimeException: Could not generate DH keypair
failed for url:https://null.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://rc4-md5.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://rc4.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://superfish.badssl.com/, reason:Connection reset
请注意,我已经尝试将$JAVA_HOME/jre/lib/security/java.security
文件的jdk.tls.disabledAlgorithms
设置为none,以确保这不是问题,并且仍然会遇到上述失败。
答案 0 :(得分:4)
您的问题的简短答案是专门信任所有证书,请使用TrustAllStrategy并执行以下操作:
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
sslContextBuilder.loadTrustMaterial(null, new TrustAllStrategy());
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
sslContextBuilder.build());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(
socketFactory).build();
但是...无效的证书可能不是您的主要问题。发生握手失败的原因有很多,但以我的经验,这通常是由于SSL / TLS版本不匹配或密码套件协商失败而引起的。这并不意味着ssl证书是“错误的”,而是服务器和客户端之间的不匹配。您可以使用Wireshark(more on that)之类的工具查看握手失败的确切位置
虽然Wireshark可以很好地查看失败的地方,但它不会帮助您提出解决方案。过去,无论何时进行调试handhake_failures,我都会发现此工具特别有用:https://testssl.sh/
您可以将该脚本指向任何失败的网站,以了解有关该目标上可用的协议以及您的客户端需要支持什么才能建立成功握手的更多信息。还将打印有关证书的信息。
例如(仅显示testssl.sh输出的两部分):
./testssl.sh www.google.com
....
Testing protocols (via sockets except TLS 1.2, SPDY+HTTP2)
SSLv2 not offered (OK)
SSLv3 not offered (OK)
TLS 1 offered
TLS 1.1 offered
TLS 1.2 offered (OK)
....
Server Certificate #1
Signature Algorithm SHA256 with RSA
Server key size RSA 2048 bits
Common Name (CN) "www.google.com"
subjectAltName (SAN) "www.google.com"
Issuer "Google Internet Authority G3" ("Google Trust Services" from "US")
Trust (hostname) Ok via SAN and CN (works w/o SNI)
Chain of trust "/etc/*.pem" cannot be found / not readable
Certificate Expiration expires < 60 days (58) (2018-10-30 06:14 --> 2019-01-22 06:14 -0700)
....
Testing all 102 locally available ciphers against the server, ordered by encryption strength
(Your /usr/bin/openssl cannot show DH/ECDH bits)
Hexcode Cipher Suite Name (OpenSSL) KeyExch. Encryption Bits
------------------------------------------------------------------------
xc030 ECDHE-RSA-AES256-GCM-SHA384 ECDH AESGCM 256
xc02c ECDHE-ECDSA-AES256-GCM-SHA384 ECDH AESGCM 256
xc014 ECDHE-RSA-AES256-SHA ECDH AES 256
xc00a ECDHE-ECDSA-AES256-SHA ECDH AES 256
x9d AES256-GCM-SHA384 RSA AESGCM 256
x35 AES256-SHA RSA AES 256
xc02f ECDHE-RSA-AES128-GCM-SHA256 ECDH AESGCM 128
xc02b ECDHE-ECDSA-AES128-GCM-SHA256 ECDH AESGCM 128
xc013 ECDHE-RSA-AES128-SHA ECDH AES 128
xc009 ECDHE-ECDSA-AES128-SHA ECDH AES 128
x9c AES128-GCM-SHA256 RSA AESGCM 128
x2f AES128-SHA RSA AES 128
x0a DES-CBC3-SHA RSA 3DES 168
因此使用此输出,我们可以看到,如果您的客户端仅支持SSLv3,则握手将失败,因为服务器不支持该协议。协议提供的问题不太可能出现,但是您可以通过获取已启用协议的列表来仔细检查Java客户端支持什么。您可以从上述代码片段中提供SSLConnectionSocketFactory的重写实现,以获取启用/支持的协议和密码套件的列表,如下所示(SSLSocket):
class MySSLConnectionSocketFactory extends SSLConnectionSocketFactory {
@Override
protected void prepareSocket(SSLSocket socket) throws IOException {
System.out.println("Supported Ciphers" + Arrays.toString(socket.getSupportedCipherSuites()));
System.out.println("Supported Protocols" + Arrays.toString(socket.getSupportedProtocols()));
System.out.println("Enabled Ciphers" + Arrays.toString(socket.getEnabledCipherSuites()));
System.out.println("Enabled Protocols" + Arrays.toString(socket.getEnabledProtocols()));
}
}
当密码套件协商失败时,我经常遇到handshake_failure。为避免此错误,客户端的受支持密码套件列表必须至少与服务器的受支持密码套件列表中的一个密码套件匹配。
如果服务器需要基于AES256的密码套件,则可能需要Java密码扩展(JCE)。这些图书馆受国家/地区限制,因此美国以外的其他地方可能无法使用。
有关加密限制的更多信息,如果您有兴趣:https://crypto.stackexchange.com/questions/20524/why-there-are-limitations-on-using-encryption-with-keys-beyond-certain-length
答案 1 :(得分:0)
我认为您所引用的帖子非常接近需要完成的工作。您是否尝试过类似的方法:
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
sslContextBuilder.setSecureRandom(new java.security.SecureRandom());
try {
sslContextBuilder.loadTrustMaterial(new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
});
clientBuilder.setSSLContext(sslContextBuilder.build());
} catch (Throwable t) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Can't set ssl context", t);
}
CloseableHttpClient apacheHttpClient = clientBuilder.build();
我还没有尝试过这段代码,但希望它可以工作。
欢呼
答案 2 :(得分:0)
如果可以使用netty
之类的其他开源库,则可以尝试以下操作:
SslProvider provider = SslProvider.JDK; // If you are not concerned about http2 / http1.1 then JDK provider will be enough
SSLContext sslCtx = SslContextBuilder.forClient()
.sslProvider(provider)
.trustManager(InsecureTrustManagerFactory.INSTANCE) // This will trust all certs
... // Any other required parameters used for ssl context.e.g. protocols , ciphers etc.
.build();
我已使用以下netty版本信任具有上述代码的任何证书:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.29.Final</version>
</dependency>
答案 3 :(得分:0)
我认为@nmorenor的答案非常接近目标。我还要做的是显式启用<M'>
(由于安全性考虑,HttpClient默认会自动禁用它)并禁用主机名验证。
SSLv3
答案 4 :(得分:0)
您也可以使用核心jdk进行此操作,但是iirc,httpclient也允许您设置SSL套接字工厂。
工厂定义并使用您与信任管理器信任的ssl上下文。该经理根本不会验证证书链,如上面的帖子所示。
您还需要一个hostnameverifier实例,该实例还将选择忽略cert主机名与url的主机(或ip)的潜在不匹配。否则,即使盲目地信任证书签名者,它仍然会失败。
我曾经将许多客户端堆栈转换为“接受自签名”,并且在大多数堆栈中都非常容易。更糟糕的情况是,第三方库不允许选择ssl套接字工厂实例,而只能选择其clasname。在那种情况下,我使用一个ThreadLocalSSLSocketFactory,它不拥有任何实际的工厂,而只是查找threadlocal来查找较高的堆栈框架(您可以控制)准备的一个。当然,这仅在第三方库没有在不同线程上进行工作的情况下才有效。我知道可以告诉http客户端使用特定的ssl套接字工厂,因此很容易。
还要花时间阅读JSSE文档,这完全值得花时间阅读。