环境:我在 64 位 Windows 7 上使用 Sun Java JDK 1.8.0_60,使用 Spring Integration 4.1.6(内部似乎使用 Apache Commons Net 3.3 进行 FTPS 访问)。
我正在尝试与我们的应用程序集成从我们客户端的 FTPS 服务器自动下载。我已经使用 Spring Integration 在 SFTP 服务器上成功地做到了这一点,其他客户端没有任何问题而没有任何问题,但这是客户端第一次要求我们使用 FTPS,让它连接起来非常令人费解。在我的实际应用程序中,我正在使用 XML bean 配置 Spring Integration,为了尝试了解什么不起作用,我正在使用以下测试代码(尽管我在这里对实际主机/用户名/密码进行了匿名处理):
final DefaultFtpsSessionFactory sessionFactory = new DefaultFtpsSessionFactory();
sessionFactory.setHost("XXXXXXXXX");
sessionFactory.setPort(990);
sessionFactory.setUsername("XXXXXXX");
sessionFactory.setPassword("XXXXXXX");
sessionFactory.setClientMode(2);
sessionFactory.setFileType(2);
sessionFactory.setUseClientMode(true);
sessionFactory.setImplicit(true);
sessionFactory.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
sessionFactory.setProt("P");
sessionFactory.setProtocol("TLSv1.2");
sessionFactory.setProtocols(new String[]{"TLSv1.2"});
sessionFactory.setSessionCreation(true);
sessionFactory.setCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"});
final FtpSession session = sessionFactory.getSession();
//try {
final FTPFile[] ftpFiles = session.list("/");
logger.debug("FtpFiles: {}", (Object[]) ftpFiles);
//} catch (Exception ignored ) {}
session.close();
我正在运行此代码以-Djavax.net.debug=all
打印所有 TLS 调试信息。
到 FTPS 服务器的主要“控制”连接工作正常,但是当它尝试打开列表的数据连接(或我尝试过的任何其他数据连接)时,我得到一个javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
,由java.io.EOFException: SSL peer shut down incorrectly
. 如果我取消注释session.list
命令周围的swallowing-exceptions catch块,那么我可以看到(通过javax.net.debug输出)服务器在拒绝数据连接SSL握手后发送了以下消息:
main, READ: TLSv1.2 Application Data, length = 129
Padded plaintext after DECRYPTION: len = 105
0000: 34 35 30 20 54 4C 53 20 73 65 73 73 69 6F 6E 20 450 TLS session
0010: 6F 66 20 64 61 74 61 20 63 6F 6E 6E 65 63 74 69 of data connecti
0020: 6F 6E 20 68 61 73 20 6E 6F 74 20 72 65 73 75 6D on has not resum
0030: 65 64 20 6F 72 20 74 68 65 20 73 65 73 73 69 6F ed or the sessio
0040: 6E 20 64 6F 65 73 20 6E 6F 74 20 6D 61 74 63 68 n does not match
0050: 20 74 68 65 20 63 6F 6E 74 72 6F 6C 20 63 6F 6E the control con
0060: 6E 65 63 74 69 6F 6E 0D 0A nection..
似乎正在发生的事情(这是我第一次处理 FTPS,虽然我之前处理过普通 FTP)是服务器确保对控制和数据连接进行身份验证和加密的方式是在“正常”之后TLS 连接建立控制连接和身份验证发生在那里,每个数据连接都要求客户端使用相同的 TLS 会话进行连接。这对我来说是有意义的,因为它应该如何工作,但 Apache Commons Net FTPS 实现似乎并没有这样做。它似乎正在尝试建立一个新的 TLS 会话,因此服务器拒绝了该尝试。
基于这个关于在 JSSE 中恢复 SSL 会话的问题,Java 似乎假设或需要每个主机/帖子组合的不同会话。我的假设是,由于 FTPS 数据连接位于与控制连接不同的端口上,因此它没有找到现有会话并试图建立一个新会话,因此连接失败。
我看到了三种主要的可能性:
- 服务器不遵循 FTPS 标准,要求数据端口上的 TLS 会话与控制端口上的相同。我可以使用 FileZilla 3.13.1 连接到服务器(使用与我在代码中尝试使用的相同的主机/用户/密码)。服务器在登录时将自己标识为“FileZilla Server 0.9.53 beta”,所以也许这是某种专有的 FileZilla 做事方式,我需要做一些奇怪的事情来说服 Java 使用相同的 TLS 会话。
- Apache Commons Net 客户端实际上并不遵循 FTPS 标准,并且只允许某些不允许保护数据连接的子集。这看起来很奇怪,因为它似乎是从 Java 内部连接到 FTPS 的标准方式。
- 我完全错过了一些东西并误诊了这一点。
我很感激你能提供的关于如何连接到这种 FTPS 服务器的任何方向。谢谢你。
实际上,某些 FTP(S) 服务器确实要求将 TLS/SSL 会话重用于数据连接。这是一种安全措施,服务器可以通过它验证数据连接是否与控制连接由同一客户端使用。
常见FTP服务器的一些参考:
NoSessionReuseRequired
指令)可以帮助您实现的是 Cyberduck FTP(S) 客户端确实支持 TLS/SSL 会话重用,并且它使用 Apache Commons Net 库:
https://trac.cyberduck.io/ticket/5087 - 在数据连接上重用会话密钥
查看它的
FTPClient.java
代码(扩展 Commons NetFTPSClient
),特别是它对method 的覆盖_prepareDataSocket_
:@Override protected void _prepareDataSocket_(final Socket socket) throws IOException { if(preferences.getBoolean("ftp.tls.session.requirereuse")) { if(socket instanceof SSLSocket) { // Control socket is SSL final SSLSession session = ((SSLSocket) _socket_).getSession(); if(session.isValid()) { final SSLSessionContext context = session.getSessionContext(); context.setSessionCacheSize(preferences.getInteger("ftp.ssl.session.cache.size")); try { final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache"); sessionHostPortCache.setAccessible(true); final Object cache = sessionHostPortCache.get(context); final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class); method.setAccessible(true); method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostName(), String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session); method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostAddress(), String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session); } catch(NoSuchFieldException e) { // Not running in expected JRE log.warn("No field sessionHostPortCache in SSLSessionContext", e); } catch(Exception e) { // Not running in expected JRE log.warn(e.getMessage()); } } else { log.warn(String.format("SSL session %s for socket %s is not rejoinable", session, socket)); } } } }
似乎该
_prepareDataSocket_
方法是FTPSClient
专门添加到 Commons Net 中以允许 TLS/SSL 会话重用实现的:https ://issues.apache.org/jira/browse/NET-426
对重用的原生支持仍然悬而未决:https :
//issues.apache.org/jira/browse/NET-408
您显然需要覆盖 Spring Integration
DefaultFtpsSessionFactory.createClientInstance()
以返回FTPSClient
具有会话重用支持的自定义实现。自 JDK 8u161 起,上述解决方案不再单独工作。
根据JDK 8u161 更新发行说明(以及@Laurent的回答):
即,您可以调用它来解决问题:
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
尽管这应该仅被视为一种解决方法。我不知道一个合适的解决方案。
另一个实现在这里:https :
//issues.apache.org/jira/browse/NET-408
关于 1.8.0_161 中的问题有一个单独的问题:
SSL 会话重用在 JDK 8u161 中的 Apache FTPS 客户端
实际上我过去也遇到过同样的问题(只是在 C++/OpenSSL 中,我不使用 Java),所以我知道该用 google 搜索什么。