使用FTPSClient上传FTP文件时遭遇CopyStreamException异常
FTP文件上传异常排查:文件创建但内容为空
背景
使用lftp通过以下固定配置访问FTP服务器:
# FTPS_OPTIONS: set ssl:verify-certificate/testhostname no; set ftp:ssl-protect-data yes; set ftp:passive-mode on;
Java应用使用apache-commons-net库实现文件上传,代码如下:
@Autowired public FtpService() { ftpsClient = new FTPSClient(); ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); } public void uploadFile(String ftpHost, File tempFile, String destination, String filename) throws UploadException { ftpsClient.connect(ftpHost, 990); ftpsClient.execPBSZ(0); ftpsClient.execPROT("P"); ftpsClient.enterLocalPassiveMode(); ftpsClient.setKeepAlive(true); ftpsClient.setControlKeepAliveTimeout(3000); if(ftpsClient.login("user", "password")) { try (InputStream fileStream = new FileInputStream(tempFile)) { if (!ftpsClient.changeWorkingDirectory(destination)) { throwUploadException("Destination directory not available in FTP server"); } boolean saved = ftpsClient.storeFile(filename, fileStream); // 因上一行抛出异常,以下代码未执行 if (!saved) { throwUploadException("Unable to save file in FTP server"); } log.info("Saved FTP file: {}/{}", destination, filename); } catch (UploadException | IOException e) { throwUploadException(e.getMessage()); } finally { ftpsClient.disconnect(); if (!tempFile.delete()) { log.warn("Unable to delete '{}' file", tempFile.getAbsolutePath()); } } } }
问题现象
改用FTPSClient后:
- ✅ 可正常切换工作目录、创建目录
- ❌ 调用
storeFile时抛出异常,服务器上创建空文件:
org.apache.commons.net.io.CopyStreamException: IOException caught while copying. Cause: javax.net.ssl.SSLProtocolException: Software caused connection abort: socket write error
- ❌ 调用
listFiles()/listDirectories()返回空列表(用户已具备全权限)
FTP交互日志:
er: testhostname:990 USER ******* 331 Usuario testuser OK. Clave requerida ( = User testuser OK. Password required) PASS ******* 230 OK. El directorio restringido actual es / ( = OK. The current restricted directory is /) CWD /test/upload 250 OK. El directorio actual es /test/upload ( = Ok. The current directory is /test/upload) PASV 227 Entering Passive Mode (<numbers...>) [Replacing PASV mode reply address <ip_address> with testhostname] STOR dummyfile.txt 150 Conexi├│n de datos aceptada ( = Data connection accepted)
排查与解决方案
1. 禁用SSL证书验证(匹配lftp配置)
lftp配置了跳过证书验证,但Java默认会强制验证服务器证书,这会导致数据通道SSL握手失败。需添加自定义信任管理器跳过验证:
// 在初始化FTPSClient时添加 private void disableCertificateValidation() throws NoSuchAlgorithmException, KeyManagementException { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {} } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new SecureRandom()); ftpsClient.setSSLContext(sslContext); ftpsClient.setTrustManager(trustAllCerts[0]); }
修改构造方法:
@Autowired public FtpService() throws NoSuchAlgorithmException, KeyManagementException { ftpsClient = new FTPSClient(); disableCertificateValidation(); // 添加这行 ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); }
2. 调整FTPS命令执行顺序
execPBSZ和execPROT需要在登录后执行,而非连接后。因为这两个命令需要在已认证的会话中发送:
public void uploadFile(String ftpHost, File tempFile, String destination, String filename) throws UploadException { ftpsClient.connect(ftpHost, 990); ftpsClient.enterLocalPassiveMode(); ftpsClient.setKeepAlive(true); ftpsClient.setControlKeepAliveTimeout(3000); if(ftpsClient.login("user", "password")) { // 移到登录后执行 ftpsClient.execPBSZ(0); ftpsClient.execPROT("P"); try (InputStream fileStream = new FileInputStream(tempFile)) { // 后续代码不变... } } }
3. 修复被动模式地址解析问题
日志中显示替换PASV地址,可能导致数据连接路由异常。禁用远程地址验证:
// 在enterLocalPassiveMode()后添加 ftpsClient.setRemoteVerificationEnabled(false);
4. 确保数据通道加密生效
execPROT("P")表示数据通道使用私有加密模式,也可尝试使用库提供的封装方法替代手动执行命令:
// 替换execPROT("P") ftpsClient.setDataChannelProtection(FTPSClient.PROTECTION_PRIVATE);
验证修改后的完整代码片段
@Autowired public FtpService() throws NoSuchAlgorithmException, KeyManagementException { ftpsClient = new FTPSClient(); disableCertificateValidation(); ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); } private void disableCertificateValidation() throws NoSuchAlgorithmException, KeyManagementException { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {} } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new SecureRandom()); ftpsClient.setSSLContext(sslContext); ftpsClient.setTrustManager(trustAllCerts[0]); } public void uploadFile(String ftpHost, File tempFile, String destination, String filename) throws UploadException { try { ftpsClient.connect(ftpHost, 990); ftpsClient.enterLocalPassiveMode(); ftpsClient.setRemoteVerificationEnabled(false); ftpsClient.setKeepAlive(true); ftpsClient.setControlKeepAliveTimeout(3000); if(ftpsClient.login("user", "password")) { ftpsClient.execPBSZ(0); ftpsClient.setDataChannelProtection(FTPSClient.PROTECTION_PRIVATE); try (InputStream fileStream = new FileInputStream(tempFile)) { if (!ftpsClient.changeWorkingDirectory(destination)) { throwUploadException("Destination directory not available in FTP server"); } boolean saved = ftpsClient.storeFile(filename, fileStream); if (!saved) { throwUploadException("Unable to save file in FTP server"); } log.info("Saved FTP file: {}/{}", destination, filename); } } else { throwUploadException("FTP login failed"); } } catch (UploadException | IOException | NoSuchAlgorithmException | KeyManagementException e) { throwUploadException(e.getMessage()); } finally { try { if (ftpsClient.isConnected()) { ftpsClient.logout(); ftpsClient.disconnect(); } } catch (IOException e) { log.error("Failed to disconnect FTP client", e); } if (!tempFile.delete()) { log.warn("Unable to delete '{}' file", tempFile.getAbsolutePath()); } } }
内容的提问来源于stack exchange,提问作者mint
相关产品推荐
相关产品推荐

