SpringBoot 集成 FTP 與 SFTP
FTP(File Transfer Protocol)和 SFTP(SSH File Transfer Protocol)雖同為文件傳輸協議,但在底層原理、安全性、傳輸方式等方面存在顯著差異,具體區別及各自優勢如下:
核心區別
- 底層依賴與傳輸機制不同
- FTP 基于TCP 協議獨立運行,使用兩個端口完成傳輸:21 端口用于控制指令(如連接、登錄),20 端口(主動模式)或動態端口(被動模式)用于實際數據傳輸,整個過程中控制流和數據流分離。
- SFTP 是SSH 協議的子協議,依賴 SSH 的加密通道運行,僅通過 22 端口即可同時處理控制指令和數據傳輸,無需額外端口,傳輸過程中所有數據(包括指令和文件內容)都在 SSH 加密通道中完成。
- 安全性差異
- FTP 是明文傳輸,用戶名、密碼及文件內容在網絡中均以未加密的形式發送,容易被監聽、竊取或篡改,安全性極低,僅適用于內網等完全可信的環境。
- SFTP 基于 SSH 的加密機制(如對稱加密、非對稱加密),所有傳輸數據都會經過加密處理,且支持身份驗證(密碼或 SSH 密鑰),能有效防止數據泄露和篡改,安全性遠高于 FTP。
- 端口與防火墻適配性
- FTP 的端口使用復雜(固定控制端口 + 動態數據端口),在防火墻或 NAT 環境下需額外配置端口映射,否則可能因端口封鎖導致連接失敗,適配性較差。
- SFTP 僅使用 22 端口,端口單一且固定,防火墻規則配置簡單,在復雜網絡環境(如跨網傳輸、云服務器)中更易適配,無需額外開放多個端口。
- 兼容性與普及度
- FTP 出現時間早(1971 年),是傳統文件傳輸的 “標準協議”,支持幾乎所有操作系統和設備,兼容性極強,老舊系統或簡易設備(如嵌入式設備)通常默認支持 FTP。
- SFTP 是較新的協議(基于 SSH 發展而來),依賴 SSH 環境,部分老舊系統或設備可能未預裝 SSH 服務,兼容性略遜于 FTP,但隨著安全需求提升,主流系統(如 Linux、Windows 10+)已普遍支持。
各自的優點與適用場景
- SFTP 的核心優點:
- 安全性碾壓:加密傳輸避免數據泄露,適合傳輸敏感文件(如用戶數據、財務報表等)。
- 端口簡化:僅需 22 端口,降低防火墻配置復雜度,尤其適合云服務器、跨網絡環境。
- 認證靈活:支持 SSH 密鑰認證,無需明文存儲密碼,進一步提升訪問安全性。
- FTP 的核心優點:
- 兼容性極強:適用于所有支持 TCP/IP 的設備,尤其在老舊系統或簡易嵌入式設備中更易部署。
- 傳輸效率略高:無加密開銷,在完全可信的內網環境中,純文件傳輸速度可能略快于 SFTP。
FTP
參考:https://bbs.huaweicloud.com/blogs/451602
- 在pom文件中添加這個
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version>
</dependency>
-
增加配置信息
ftp: host: ip: 你的ftp ip地址 port: 21 username: 你的賬號 password: 你的密碼 basePath: 要上傳的路徑(要有權限)
@Data
@Component
@ConfigurationProperties(prefix = "ftp")
public class FtpConfig {
@Value("${ftp.host.ip}")
private String host;
@Value("${ftp.port}")
private int port;
@Value("${ftp.username}")
private String username;
@Value("${ftp.password}")
private String password;
@Value("${ftp.basePath}")
private String basePath;
}
- 編寫相關的工具類
@Component
public class FtpUtil {
@Autowired
private SFtpConfig ftpConfig;
/**
* 連接FTP服務器
*/
private FTPClient connect() throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding("UTF-8");
ftpClient.connect(ftpConfig.getHost(), ftpConfig.getPort());
ftpClient.login(ftpConfig.getUsername(), ftpConfig.getPassword());
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
ftpClient.setBufferSize(1024 * 1024); // 設置緩沖區大小
// 檢查連接是否成功
int replyCode = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(replyCode)) {
ftpClient.disconnect();
throw new IOException("FTP服務器拒絕連接");
}
return ftpClient;
}
/**
* 上傳文件到FTP服務器
* @param remotePath 遠程路徑
* @param fileName 文件名
* @param inputStream 輸入流
* @return 是否上傳成功
*/
public boolean uploadFile(String remotePath, String fileName, InputStream inputStream) {
FTPClient ftpClient = null;
try {
ftpClient = connect();
// 切換目錄
if (!ftpClient.changeWorkingDirectory(remotePath)) {
// 如果目錄不存在,則創建
String[] dirs = remotePath.split("/");
String tempPath = "";
for (String dir : dirs) {
if (dir.length() > 0) {
tempPath += "/" + dir;
if (!ftpClient.changeWorkingDirectory(tempPath)) {
if (!ftpClient.makeDirectory(tempPath)) {
return false;
}
}
}
}
}
// 上傳文件
boolean success = ftpClient.storeFile(fileName, inputStream);
return success;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 從FTP服務器下載文件
* @param remotePath 遠程路徑
* @param fileName 文件名
* @param outputStream 輸出流
* @return 是否下載成功
*/
public boolean downloadFile(String remotePath, String fileName, OutputStream outputStream) {
FTPClient ftpClient = null;
try {
ftpClient = connect();
// 切換目錄
if (!ftpClient.changeWorkingDirectory(remotePath)) {
return false;
}
// 下載文件
boolean success = ftpClient.retrieveFile(fileName, outputStream);
return success;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 刪除FTP服務器上的文件
* @param remotePath 遠程路徑
* @param fileName 文件名
* @return 是否刪除成功
*/
public boolean deleteFile(String remotePath, String fileName) {
FTPClient ftpClient = null;
try {
ftpClient = connect();
// 切換目錄
if (!ftpClient.changeWorkingDirectory(remotePath)) {
return false;
}
// 刪除文件
boolean success = ftpClient.deleteFile(fileName);
return success;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 檢查文件是否存在
* @param remotePath 遠程路徑
* @param fileName 文件名
* @return 文件是否存在
*/
public boolean fileExists(String remotePath, String fileName) {
FTPClient ftpClient = null;
try {
ftpClient = connect();
// 切換目錄
if (!ftpClient.changeWorkingDirectory(remotePath)) {
return false;
}
FTPFile[] files = ftpClient.listFiles();
for (FTPFile file : files) {
if (file.getName().equals(fileName)) {
return true;
}
}
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
寫在工具類中,后續可以直接調用
SFTP
參考: https://blog.csdn.net/qq_33204709/article/details/135974528
配置類
<!-- SFTP -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
連接工具類
@Data
@Component
@ConfigurationProperties(prefix = "ftp")
public class SFtpConfig {
@Value("${ftp.host.ip}")
private String host;
@Value("${ftp.port}")
private int port;
@Value("${ftp.username}")
private String username;
@Value("${ftp.password}")
private String password;
@Value("${ftp.basePath}")
private String basePath;
@Value("${ftp.timeOut}")
private int timeOut;
/*
* sftp, 默認協議
*/
private String protocol = "sftp";
}
接口(便于復用)
public interface SFtpService {
/**
* 上傳文件
* @param sftpPath SFTP目標路徑
* @param file 要上傳的文件
* @return 是否上傳成功(true:成功,false:失敗)
*/
boolean upload(String sftpPath, MultipartFile file);
/**
* 下載文件
* @param sftpPath SFTP文件路徑
* @param response HTTP響應對象,用于輸出文件流
* @return 是否下載成功(true:成功,false:失敗)
*/
boolean download(String sftpPath, HttpServletResponse response);
/**
* 下載文件 全量加載到內存,僅適合小文件
* @param sftpPath SFTP文件路徑
* @return 文件字節數組
*/
public byte[] download(String sftpPath);
/**
* 重命名文件(或移動文件)
* @param oldPath 原文件路徑
* @param newPath 新文件路徑
* @return 是否重命名成功(true:成功,false:失敗)
*/
boolean rename(String oldPath, String newPath);
/**
* 刪除文件(或目錄)
* @param sftpPath 要刪除的文件或目錄路徑
* @return 是否刪除成功(true:成功,false:失敗)
*/
boolean delete(String sftpPath);
}
實現類
@Service
@Slf4j
public class SFtpServiceImpl implements SFtpService {
@Resource
private SftpUtils sftpUtils;
@Override
public boolean upload(String sftpPath, MultipartFile file) {
// 上傳文件
ChannelSftp sftp = null;
try (InputStream in = file.getInputStream()) {
// 開啟sftp連接
sftp = sftpUtils.createSftp();
// 進入sftp文件目錄
sftp.cd(sftpPath);
log.info("切換到目錄為:{}", sftpPath);
// 上傳文件
sftp.put(in, file.getOriginalFilename());
log.info("上傳文件成功,目標目錄:{}", sftpPath);
return true;
} catch (SftpException | JSchException | IOException e) {
log.error("上傳文件失敗,原因:{}", e.getMessage(), e);
return false;
} finally {
// 關閉sftp
sftpUtils.disconnect(sftp);
}
}
@Override
public boolean download(String sftpPath, HttpServletResponse response) {
ChannelSftp sftp = null;
try {
// 1. 建立SFTP連接
sftp = sftpUtils.createSftp();
// 2. 檢查文件是否存在(直接嘗試訪問文件)
try {
// 如果文件不存在,會拋出 SftpException
sftp.lstat(sftpPath);
} catch (SftpException e) {
log.error("文件不存在或無法訪問: {}", sftpPath);
return false;
}
// 3. 從路徑中提取文件名(如 "/upload/test.txt" -> "test.txt")
String fileName = sftpPath.substring(sftpPath.lastIndexOf("/") + 1);
// 4. 設置HTTP響應頭(支持中文文件名)
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
);
// 5. 下載文件(直接使用完整路徑)
sftp.get(sftpPath, response.getOutputStream());
log.info("文件下載成功: {}", sftpPath);
return true;
} catch (SftpException e) {
log.error("SFTP操作失敗: {}", e.getMessage(), e);
return false;
} catch (IOException | JSchException e) {
log.error("系統異常: {}", e.getMessage(), e);
return false;
} finally {
sftpUtils.disconnect(sftp);
}
}
@Override
public byte[] download(String sftpPath) {
ChannelSftp sftp = null;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
// 1. 建立SFTP連接
sftp = sftpUtils.createSftp();
// 2. 檢查文件是否存在
try {
sftp.lstat(sftpPath);
} catch (SftpException e) {
log.error("文件不存在: {}", sftpPath);
return null;
}
// 3. 下載文件到內存
sftp.get(sftpPath, outputStream);
log.info("文件下載成功: {}", sftpPath);
return outputStream.toByteArray();
} catch (SftpException | JSchException | IOException e) {
log.error("下載失敗: {}", e.getMessage(), e);
return null;
} finally {
sftpUtils.disconnect(sftp);
}
}
@Override
public boolean rename(String oldPath, String newPath) {
// 重命名文件(移動)
ChannelSftp sftp = null;
try {
// 開啟sftp連接
sftp = sftpUtils.createSftp();
// 修改sftp文件路徑
sftp.rename(oldPath, newPath);
log.info("sftp文件重命名成功,歷史路徑:{},新路徑:{}", oldPath, newPath);
return true;
} catch (SftpException | JSchException e) {
log.error("sftp文件重命名失敗,原因:{}", e.getMessage(), e);
return false;
} finally {
// 關閉sftp
sftpUtils.disconnect(sftp);
}
}
@Override
public boolean delete(String sftpPath) {
// 刪除文件
ChannelSftp sftp = null;
try {
// 開啟sftp連接
sftp = sftpUtils.createSftp();
// 判斷sftp文件存在
boolean isExist = isFileExist(sftpPath, sftp);
if (!isExist) {
log.error("sftp文件刪除失敗,sftp文件不存在:" + sftpPath);
return false;
}
// 刪除文件
SftpATTRS sftpATTRS = sftp.lstat(sftpPath);
if (sftpATTRS.isDir()) {
sftp.rmdir(sftpPath);
} else {
sftp.rm(sftpPath);
}
log.info("sftp文件刪除成功,目標文件:{}.", sftpPath);
return true;
} catch (SftpException | JSchException e) {
log.error("sftp文件刪除失敗,原因:{}", e.getMessage(), e);
return false;
} finally {
// 關閉sftp
sftpUtils.disconnect(sftp);
}
}
/**
* 判斷目錄是否存在
*/
private boolean isFileExist(String sftpPath, ChannelSftp sftp) {
try {
// 獲取文件信息
SftpATTRS sftpATTRS = sftp.lstat(sftpPath);
return sftpATTRS != null;
} catch (Exception e) {
log.error("判斷文件是否存在失敗,原因:{}", e.getMessage(), e);
return false;
}
}
}
control層調用
@RestController
@RequestMapping("/sftp")
public class SFtpController {
@Resource
private SFtpService sftpService;
/**
* 上傳文件
*/
@PostMapping("/upload")
public R<Object> upload(@RequestParam String sftpPath, @RequestParam MultipartFile file) {
sftpService.upload(sftpPath, file);
return R.success();
}
/**
* 下載文件
*/
@GetMapping("/download")
public void download(@RequestParam String sftpPath, HttpServletResponse response) {
sftpService.download(sftpPath, response);
}
/**
* 重命名文件(移動)
*/
@GetMapping("/rename")
public R<Object> rename(@RequestParam String oldPath, @RequestParam String newPath) {
sftpService.rename(oldPath, newPath);
return R.success();
}
/**
* 刪除文件
*/
@GetMapping("/delete")
public R<Object> delete(@RequestParam String sftpPath) {
sftpService.delete(sftpPath);
return R.success();
}
}
這里用SFTP容易有個坑,可能你寫的你能連接上,到了生產上連接不上。這個后續公眾號會發,或者詳細見我的csdn這篇文章https://blog.csdn.net/m0_58680378/article/details/147094033?spm=1011.2415.3001.5331

浙公網安備 33010602011771號