高階配置

DefaultFtpSessionFactory 在底層客戶端 API 之上提供了一個抽象層,自 Spring Integration 2.0 版本起,該抽象層基於 Apache Commons Net。這使得您無需關注 org.apache.commons.net.ftp.FTPClient 的低層配置細節。Session Factory 暴露了一些通用屬性(自 4.0 版本起,包括 connectTimeoutdefaultTimeoutdataTimeout)。然而,有時您需要訪問更低層的 FTPClient 配置來實現更高階的設定(例如為主動模式設定埠範圍)。為此,AbstractFtpSessionFactory(所有 FTP Session Factory 的基類)透過以下列表中所示的兩個後處理方法提供了鉤子

/**
 * Will handle additional initialization after client.connect() method was invoked,
 * but before any action on the client has been taken
 */
protected void postProcessClientAfterConnect(T t) throws IOException {
    // NOOP
}
/**
 * Will handle additional initialization before client.connect() method was invoked.
 */
protected void postProcessClientBeforeConnect(T client) throws IOException {
    // NOOP
}

如您所見,這兩個方法沒有預設實現。但是,透過擴充套件 DefaultFtpSessionFactory,您可以覆蓋這些方法來提供更高階的 FTPClient 配置,示例如下所示

public class AdvancedFtpSessionFactory extends DefaultFtpSessionFactory {

    protected void postProcessClientBeforeConnect(FTPClient ftpClient) throws IOException {
       ftpClient.setActivePortRange(4000, 5000);
    }
}

FTPS 和共享 SSLSession

當使用基於 SSL 或 TLS 的 FTP 時,有些伺服器要求控制連線和資料連線使用相同的 SSLSession。這是為了防止“資料連線被竊取”。更多資訊請參見 scarybeastsecurity.blogspot.cz/2009/02/vsftpd-210-released.html

當前,Apache FTPSClient 不支援此特性。參見 NET-408

以下解決方案來自 Stack Overflow,使用反射訪問了 sun.security.ssl.SSLSessionContextImpl,因此可能無法在其他 JVM 上工作。該 Stack Overflow 回答於 2015 年提交,Spring Integration 團隊已在 JDK 1.8.0_112 上測試過該解決方案。

以下示例展示瞭如何建立 FTPS session

@Bean
public DefaultFtpsSessionFactory sf() {
    DefaultFtpsSessionFactory sf = new DefaultFtpsSessionFactory() {

        @Override
        protected FTPSClient createClientInstance() {
            return new SharedSSLFTPSClient();
        }

    };
    sf.setHost("...");
    sf.setPort(21);
    sf.setUsername("...");
    sf.setPassword("...");
    sf.setNeedClientAuth(true);
    return sf;
}

private static final class SharedSSLFTPSClient extends FTPSClient {

    @Override
    protected void _prepareDataSocket_(final Socket socket) throws IOException {
        if (socket instanceof SSLSocket) {
            // Control socket is SSL
            final SSLSession session = ((SSLSocket) _socket_).getSession();
            final SSLSessionContext context = session.getSessionContext();
            context.setSessionCacheSize(0); // you might want to limit the cache
            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);
                String key = String.format("%s:%s", socket.getInetAddress().getHostName(),
                        String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT);
                method.invoke(cache, key, session);
                key = String.format("%s:%s", socket.getInetAddress().getHostAddress(),
                        String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT);
                method.invoke(cache, key, session);
            }
            catch (NoSuchFieldException e) {
                // Not running in expected JRE
                logger.warn("No field sessionHostPortCache in SSLSessionContext", e);
            }
            catch (Exception e) {
                // Not running in expected JRE
                logger.warn(e.getMessage());
            }
        }

    }

}