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号