SFTP介绍
- SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法,语法几乎和FTP一致。
- 相比于FTP,SFTP更安全,但更安全带来副作用就是的效率比FTP要低些。
- SFTP是SSH的一部分,内部是采用SSH连接, 所以在以下代码中进行文件的操作都会先cd到SFTP存放文件的根路径下。
- Reference:SFTP与FTP比较、浅谈SFTP与FTP。
实战
1. 相关依赖(基于SpringBoot)
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.54</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>
2. 相关配置
#============================================================================ # SFTP Client Setting #============================================================================ # 协议 sftp.client.protocol=sftp # ip地址 sftp.client.host=127.0.0.1 # 端口 sftp.client.port=22 # 用户名 sftp.client. username =sftp # 密码 sftp.client.password=sftp # 根路径 sftp.client.root=/home/sftp/ # 密钥文件路径 sftp.client.privateKey= # 密钥的密码 sftp.client.passphrase= # sftp.client. session StrictHostKeyChecking=no # session连接超时时间 sftp.client.session connect Timeout=15000 # channel 连接超时时间 sftp.client.channelConnectedTimeout=15000
- 这里暂时没有使用到使用加密密钥的方式登陆,所以暂不填写
3. 将application.properties中配置转为一个Bean
@Getter @Setter @Component @ConfigurationProperties(ignoreUnknownFields = false, prefix = "sftp.client") public class SftpProperties { private String host; private Integer port; private String protocol; private String username; private String password; private String root; private String privateKey; private String passphrase; private String sessionStrictHostKeyChecking; private Integer sessionConnectTimeout; private Integer channelConnectedTimeout; }
4. 将上传下载文件封装成Service
- FileSystemService
/** * @author jason.tang * @create 2019-03-07 13:33 * @description */public interface FileSystemService { boolean uploadFile(String targetPath, InputStream inputStream ) throws Exception; boolean uploadFile(String targetPath, File file ) throws Exception; File downloadFile(String targetPath) throws Exception; boolean deleteFile(String targetPath) throws Exception; }
- 实现类:FileSystemServiceImpl(此处省略相关上传下载代码)
/** * @author jason.tang * @create 2019-03-07 13:33 * @description */@Slf4j @Service("fileSystemService") public class FileSystemServiceImpl implements FileSystemService { @Autowired private SftpProperties config; // 设置第一次登陆的时候提示,可选值:(ask | yes | no) private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; /** * 创建SFTP连接 * @return * @throws Exception */ private ChannelSftp createSftp() throws Exception { JSch jsch = new JSch(); log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use password[" + config.getPassword() + "]"); Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort()); session.setPassword(config.getPassword()); session.connect(config.getSessionConnectTimeout()); log.info("Session connected to {}.", config.getHost()); Channel channel = session.openChannel(config.getProtocol()); channel.connect(config.getChannelConnectedTimeout()); log.info("Channel created to {}.", config.getHost()); return (ChannelSftp) channel; } /** * 加密秘钥方式登陆 * @return */ private ChannelSftp connectByKey() throws Exception { JSch jsch = new JSch(); // 设置密钥和密码 ,支持密钥的方式登陆 if (StringUtils.isNotBlank(config.getPrivateKey())) { if (StringUtils.isNotBlank(config.getPassphrase())) { // 设置带口令的密钥 jsch.addIdentity(config.getPrivateKey(), config.getPassphrase()); } else { // 设置不带口令的密钥 jsch.addIdentity(config.getPrivateKey()); } } log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use private key[" + config.getPrivateKey() + "] with passphrase[" + config.getPassphrase() + "]"); Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort()); // 设置登陆超时时间 session.connect(config.getSessionConnectTimeout()); log.info("Session connected to " + config.getHost() + "."); // 创建sftp通信通道 Channel channel = session.openChannel(config.getProtocol()); channel.connect(config.getChannelConnectedTimeout()); log.info("Channel created to " + config.getHost() + "."); return (ChannelSftp) channel; } /** * 创建session * @param jsch * @param host * @param username * @param port * @return * @throws Exception */ private Session createSession(JSch jsch, String host, String username, Integer port) throws Exception { Session session = null; if (port <= 0) { session = jsch.getSession(username, host); } else { session = jsch.getSession(username, host, port); } if (session == null) { throw new Exception(host + " session is null"); } session.setConfig(SESSION_CONFIG_STRICT_HOST_KEY_CHECKING, config.getSessionStrictHostKeyChecking()); return session; } /** * 关闭连接 * @param sftp */ private void disconnect(ChannelSftp sftp) { try { if (sftp != null) { if (sftp.isConnected()) { sftp.disconnect(); } else if (sftp.isClosed()) { log.info("sftp is closed already"); } if (null != sftp.getSession()) { sftp.getSession().disconnect(); } } } catch (JSchException e) { e.printStackTrace(); } } }
5. 上传文件
- 5.1 将inputStream上传到指定路径下(单级或多级目录)
@ Override
public boolean uploadFile(String targetPath, InputStream inputStream) throws Exception {
ChannelSftp sftp = this.createSftp();
try {
sftp.cd(config.getRoot());
log.info("Change path to {}", config.getRoot());
int index = targetPath.lastIndexOf("/");
String fileDir = targetPath.substring(0, index);
String fileName = targetPath.substring(index + 1);
boolean dirs = this.createDirs(fileDir, sftp);
if (!dirs) {
log.error("Remote path error. path:{}", targetPath);
throw new Exception("Upload File failure");
}
sftp.put(inputStream, fileName);
return true;
} catch (Exception e) {
log.error("Upload file failure. TargetPath: {}", targetPath, e);
throw new Exception("Upload File failure");
} finally {
this.disconnect(sftp);
}
}
- 5.2 创建多级目录
private boolean createDirs(String dirPath, ChannelSftp sftp) { if (dirPath != null && !dirPath.isEmpty() && sftp != null) { String[] dirs = Arrays.stream(dirPath.split("/")) .filter(StringUtils::isNotBlank) .toArray(String[]::new); for (String dir : dirs) { try { sftp.cd(dir); log.info("Change directory {}", dir); } catch (Exception e) { try { sftp.mkdir(dir); log.info("Create directory {}", dir); } catch (SftpException e1) { log.error("Create directory failure, directory:{}", dir, e1); e1.printStackTrace(); } try { sftp.cd(dir); log.info("Change directory {}", dir); } catch (SftpException e1) { log.error("Change directory failure, directory:{}", dir, e1); e1.printStackTrace(); } } } return true; } return false; }
- 5.3 将文件上传到指定目录
@Override public boolean uploadFile(String targetPath, File file) throws Exception { return this.uploadFile(targetPath, new FileInputStream(file)); }
6. 下载文件
@Override public File downloadFile(String targetPath) throws Exception { ChannelSftp sftp = this.createSftp(); OutputStream outputStream = null; try { sftp.cd(config.getRoot()); log.info("Change path to {}", config.getRoot()); File file = new File(targetPath.substring(targetPath.lastIndexOf("/") + 1)); outputStream = new FileOutputStream(file); sftp.get(targetPath, outputStream); log.info("Download file success. TargetPath: {}", targetPath); return file; } catch (Exception e) { log.error("Download file failure. TargetPath: {}", targetPath, e); throw new Exception("Download File failure"); } finally { if (outputStream != null) { outputStream.close(); } this.disconnect(sftp); } }
7. 删除文件
/** * 删除文件 * @param targetPath * @return * @throws Exception */@Override public boolean deleteFile(String targetPath) throws Exception { ChannelSftp sftp = null; try { sftp = this.createSftp(); sftp.cd(config.getRoot()); sftp.rm(targetPath); return true; } catch (Exception e) { log.error("Delete file failure. TargetPath: {}", targetPath, e); throw new Exception("Delete File failure"); } finally { this.disconnect(sftp); } }
8. 最后
- 涉及到对文件的操作,一定记得将流关闭。
- 在使用中比如下载文件,请将生成的文件在使用后删除(file.delete()),避免在服务器中占据大量资源。
- application.proerties中SFTP相关配置,请自行更换。如有不对之处,请指出,感谢阅读!