好久没有更新博客了,以前总说自己没有时间(就是太懒了),这一年我会坚持写博客,记录自己的点点滴滴,同时也希望能帮助到别人
好了闲言少续,言归正传,最近项目中要用到java 访问sftp来读取(即下载)linux服务器上的文件,把解决过程和大家分享一下,其中参考了好多的网上资料,整理如下。
java源代码在后面,其中对方法都做了说明,也用main 主函数进行了测试,测试证明可以进行文件下载
1.sftp简介
sftp是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法。sftp 与 ftp 有着几乎一样的语法和功能。SFTP 为 SSH的一部分,是一种传输档案至 Blogger 伺服器的安全方式。其实在SSH软件包中,已经包含了一个叫作SFTP(Secure File Transfer Protocol)的安全文件传输子系统,SFTP本身没有单独的守护进程,它必须使用sshd守护进程(端口号默认是22)来完成相应的连接操作,所以从某种意义上来说,SFTP并不像一个服务器程序,而更像是一个客户端程序。SFTP同样是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多,如果您对网络安全性要求更高时,可以使用SFTP代替FTP。
2.开始配置linux
提供sftp服务,可以用系统自带的internal-sftp,也可以使用vsftpd,这里需求不多,直接选用internal-sftp。
限制用户只能在自己的home目录下活动,这里需要使用到chroot,openssh 4.8p1以后都支持chroot,我现在用的是CentOS 6.3,自带的openssh已经是5.3p1,足够了。
可以输入:# ssh -V
来查看openssh的版本,如果低于4.8p1,需要自行升级安装。
假设,有一个名为sftp的组,这个组中的用户只能使用sftp,不能使用ssh,且sftp登录后只能在自己的home目录下活动
1、创建sftp组
# groupadd sftp
2、创建一个sftp用户,名为mysftp
# useradd -g sftp -s /bin/false mysftp
# passwd mysftp
3、sftp组的用户的home目录统一指定到/data/sftp下,按用户名区分,这里先新建一个mysftp目录,然后指定mysftp的home为/data/sftp/mysftp
# mkdir -p /data/sftp/mysftp
# usermod -d /data/sftp/mysftp mysftp
4、配置sshd_config
编辑 /etc/ssh/sshd_config
# vim +132 /etc/ssh/sshd_config
找到如下这行,并注释掉
Subsystem sftp /usr/libexec/openssh/sftp-server
添加如下几行
Subsystem sftp internal-sftp
Match Group sftp
ChrootDirectory /data/sftp/%u
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
保存推出(不过我是用图形化界面编辑的)
解释一下添加的几行的意思
Subsystem sftp internal-sftp
这行指定使用sftp服务使用系统自带的internal-sftp
Match Group sftp
这行用来匹配sftp组的用户,如果要匹配多个组,多个组之间用逗号分割
当然,也可以匹配用户
Match User mysftp
这样就可以匹配用户了,多个用户名之间也是用逗号分割,但我们这里按组匹配更灵活和方便
ChrootDirectory /data/sftp/%u
用chroot将用户的根目录指定到/data/sftp/%u,%u代表用户名,这样用户就只能在/data/sftp/%u下活动,chroot的含义,可以参考这里:http://www.ibm.com/developerworks/cn/linux/l-cn-chroot/
ForceCommand internal-sftp
指定sftp命令
AllowTcpForwarding no
X11Forwarding no
这两行,如果不希望该用户能使用端口转发的话就加上,否则删掉
5、设定Chroot目录权限
# chown root:sftp /data/sftp/mysftp
# chmod 755 /data/sftp/mysftp
错误的目录权限设定会导致在log中出现”fatal: bad ownership or modes for chroot directory XXXXXX”的内容
目录的权限设定有两个要点:
1、由ChrootDirectory指定的目录开始一直往上到系统根目录为止的目录拥有者都只能是root
2、由ChrootDirectory指定的目录开始一直往上到系统根目录为止都不可以具有群组写入权限
所以遵循以上两个原则
1)我们将/data/sftp/mysftp的所有者设置为了root,所有组设置为sftp
2)我们将/data/sftp/mysftp的权限设置为755,所有者root有写入权限,而所有组sftp无写入权限
6、建立SFTP用户登入后可写入的目录
照上面设置后,在重启sshd服务后,用户mysftp已经可以登录,但使用chroot指定根目录后,根应该是无法写入的,所以要新建一个目录供mysftp上传文件。这个目录所有者为mysftp,所有组为sftp,所有者有写入权限,而所有组无写入权限
# mkdir /data/sftp/mysftp/upload
# chown mysftp:sftp /data/sftp/mysftp/upload
# chmod 755 /data/sftp/mysftp/upload
7、重启sshd服务
# service sshd restart
到这里,mysftp已经可以通过sftp客户端登录并可以上传文件到upload目录。
如果还是不能在此目录下上传文件,提示没有权限,检查SElinux是否关闭,可以使用如下指令关闭SElinux
修改/etc/selinux/config文件中的SELINUX="" 为 disabled ,然后重启。或者
# setenforce 0 ,我把上面所有的工作都做好后,没有关闭selinux 怎么试都不好使,跳楼的心都有,不过好在查了不少资料后问题解决了
个人有个建议,如果是自己的linux环境大可把这个功能关闭。
*输入命令:chkconfig sshd on 即可。
一开始,我以为是权限问题导致的无法上传文件,即使给777权限给文件夹也不行. 然后干脆关闭了SElinux,终于可以上传了。
在给一个java连接sftp的代码,可以根据自己的需要来更改代码
如果你用maven 直接复制下面代码到pom.xml文件即可
也可以下载jsch的jar包(自己去找吧)
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.46</version>
</dependency>
package com.idea.core.utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
/**
* sftp 基础类库
* 类描述: 通过配置代码连接sftp及提供相关工具函数
* 说明: 1:本工具不提供除配置连接方式之外的连接方式
* 2:构造函数必须传入参数获取配置信息
* 3:本工具只支持sftp配置连接方式,不支持ftp等其它方式.
*/
public class SftpUtils {
private Session session;
private Channel channel;
private String host;
private String username;
private String password;
private int port;
private FileOutputStream fos = null;
private Logger log = LoggerFactory.getLogger(FtpUtils.class);
/** 对外可访问 ChannelSftp对象提供的所有底层方法*/
public static ChannelSftp chnSftp;
/**文件类型*/
public static final int FILE_TYPE = 1;
/**目录类型*/
public static final int DIR_TYPE = 2;
/**
* 说明:构造函数
* @param sftpPathCode
* @throws Exception
*/
public SftpUtils(String username, String password, String host, int port) throws Exception {
super();
this.username = username;
this.password = password;
this.host = host;
this.port = port;
}
/**
* 通过配置 打开sftp连接资源
*/
public void open () {
try {
this.connect(this.getHost(), this.getPort(), this.getUsername(), this.getPassword());
log.info("打开资源成功,可以进行连接");
} catch (Exception e) {
log.info("打开资源时错误",e);
}
}
/**
* 连接SFTP
* @param host
* @param port
* @param username
* @param password
* @throws JSchException
* @throws SftpException
*/
public void connect (String host, int port, String username, String password) throws JSchException,SftpException {
JSch jsch = new JSch();
session = jsch.getSession(username, host, port);
//System.out.println("Session created.");
session.setPassword(password);
Properties sshConfig = new Properties();
sshConfig.put("StrictHostKeyChecking", "no");
session.setConfig(sshConfig);
session.connect();
//System.out.println("Session connected.");
channel = session.openChannel("sftp");
//System.out.println("Channel is Opened!");
channel.connect();
//System.out.println("Channel is connected!");
chnSftp = (ChannelSftp) channel;
//System.out.println("Connected to " + host + " is sucess!");
}
/**
* 进入指定的目录并设置为当前目录
* @param sftpPath "/dcds/abc/" ,将abc目录设置成当前目录
* @throws Exception
*/
public void cd (String sftpPath) throws SftpException {
chnSftp.cd(sftpPath);
}
/**
* 得到当前用户当前工作目录地址
* @return 返回当前工作目录地址
* @throws SftpException
*
*/
public String pwd () throws SftpException {
return chnSftp.pwd();
}
/**
* 根据目录地址,文件类型返回文件或目录列表
* @param directory 如:/home/newtouch/kftesthis/201006/08/
* @param fileType 如:FILE_TYPE 传 1 或者DIR_TYPE 传2
* @return 文件或者目录列表 List
* @throws SftpException
* @throws Exception
*
*/
public List<String> listFiles (String directory, int fileType) throws SftpException {
List<String> fileList = new ArrayList<String>();
if (isDirExist(directory)) {
boolean itExist = false;
@SuppressWarnings("rawtypes")
Vector vector;
vector = chnSftp.ls(directory);
for (int i = 0; i < vector.size(); i++) {
Object obj = vector.get(i);
String str = obj.toString().trim();
int tag = str.lastIndexOf(":") + 3;
String strName = str.substring(tag).trim();
itExist = isDirExist(directory + "/" + strName);
if (fileType == FILE_TYPE) {
if (!(itExist)) {
fileList.add(directory + "/" + strName);
}
}
if (fileType == DIR_TYPE) {
if (itExist) {
//目录列表中去掉目录名为.和..
if (!(strName.equals(".") || strName.equals(".."))) {
fileList.add(directory + "/" + strName);
}
}
}
}
}
return fileList;
}
/**
* 判断目录是否存在
* @param directory
* @return
* @throws SftpException
*/
public boolean isDirExist (String directory) throws SftpException {
boolean isDirExistFlag = false;
try {
SftpATTRS sftpATTRS = chnSftp.lstat(directory);
isDirExistFlag = true;
return sftpATTRS.isDir();
} catch (Exception e) {
if (e.getMessage().toLowerCase().equals("no such file")) {
isDirExistFlag = false;
}
}
return isDirExistFlag;
}
/**
* 获取远程文件的流文件
* @param sftpFilePath
* @return
* @throws SftpException
*/
public InputStream getFile (String sftpFilePath) throws SftpException {
if (isFileExist(sftpFilePath)) {
return chnSftp.get(sftpFilePath);
}
return null;
}
/**
* 获取远程文件流
* @param sftpFilePath
* @return
* @throws SftpException
*/
public InputStream getInputStreamFile (String sftpFilePath) throws SftpException {
return getFile(sftpFilePath);
}
/**
* 获取远程文件字节流
* @param sftpFilePath
* @return
* @throws SftpException
* @throws IOException
*/
public ByteArrayInputStream getByteArrayInputStreamFile (String sftpFilePath) throws SftpException,IOException {
if (isFileExist(sftpFilePath)) {
byte[] srcFtpFileByte = inputStreamToByte(getFile(sftpFilePath));
ByteArrayInputStream srcFtpFileStreams = new ByteArrayInputStream(srcFtpFileByte);
return srcFtpFileStreams;
}
return null;
}
/**
* 获取远程文件的字节数组(将文件流转换成字节数组)
* @param sftpFilePath
* @return
* @throws SftpException
* @throws IOException
*/
public byte[] getByteArray (String sftpFilePath) throws SftpException,IOException {
if (isFileExist(sftpFilePath)) {
byte[] srcFtpFileByte = inputStreamToByte(getFile(sftpFilePath));
return srcFtpFileByte;
}
return null;
}
/**
* 删除远程文件
* 说明:返回信息定义以:分隔第一个为代码,第二个为返回信息
* @param sftpFilePath
* @return
* @throws SftpException
*/
public String delFile (String sftpFilePath) throws SftpException {
String retInfo = "";
if (isFileExist(sftpFilePath)) {
chnSftp.rm(sftpFilePath);
retInfo = "1:File deleted.";
}
else {
retInfo = "2:Delete file error,file not exist.";
}
return retInfo;
}
/**
* 移动远程文件到目标目录
* @param srcSftpFilePath
* @param distSftpFilePath
* @return 返回移动成功或者失败代码和信息
* @throws SftpException
* @throws IOException
*/
public String moveFile (String srcSftpFilePath, String distSftpFilePath) throws SftpException,IOException {
String retInfo = "";
boolean dirExist = false;
boolean fileExist = false;
fileExist = isFileExist(srcSftpFilePath);
dirExist = isDirExist(distSftpFilePath);
if (!fileExist) {
//文件不存在直接反回.
return "0:file not exist !";
}
if (!(dirExist)) {
//1建立目录
createDir(distSftpFilePath);
//2设置dirExist为true
dirExist = true;
}
if (dirExist && fileExist) {
String fileName = srcSftpFilePath.substring(srcSftpFilePath.lastIndexOf("/"), srcSftpFilePath.length());
ByteArrayInputStream srcFtpFileStreams = getByteArrayInputStreamFile(srcSftpFilePath);
//二进制流写文件
chnSftp.put(srcFtpFileStreams, distSftpFilePath + fileName);
chnSftp.rm(srcSftpFilePath);
retInfo = "1:move success!";
}
return retInfo;
}
/**
* 复制远程文件到目标目录
* @param srcSftpFilePath
* @param distSftpFilePath
* @return
* @throws SftpException
* @throws IOException
*/
public String copyFile (String srcSftpFilePath, String distSftpFilePath) throws SftpException,IOException {
String retInfo = "";
boolean dirExist = false;
boolean fileExist = false;
fileExist = isFileExist(srcSftpFilePath);
dirExist = isDirExist(distSftpFilePath);
if (!fileExist) {
//文件不存在直接反回.
return "0:file not exist !";
}
if (!(dirExist)) {
//1建立目录
createDir(distSftpFilePath);
//2设置dirExist为true
dirExist = true;
}
if (dirExist && fileExist) {
String fileName = srcSftpFilePath.substring(srcSftpFilePath.lastIndexOf("/"), srcSftpFilePath.length());
ByteArrayInputStream srcFtpFileStreams = getByteArrayInputStreamFile(srcSftpFilePath);
//二进制流写文件
chnSftp.put(srcFtpFileStreams, distSftpFilePath + fileName);
retInfo = "1:copy file success!";
}
return retInfo;
}
/**
* 创建远程目录
* @param sftpDirPath
* @return 返回创建成功或者失败的代码和信息
* @throws SftpException
*/
public String createDir (String sftpDirPath) throws SftpException {
this.cd("/");
if (this.isDirExist(sftpDirPath)) {
return "0:dir is exist !";
}
String pathArry[] = sftpDirPath.split("/");
for (String path : pathArry) {
if (path.equals("")) {
continue;
}
if (isDirExist(path)) {
this.cd(path);
}
else {
//建立目录
chnSftp.mkdir(path);
//进入并设置为当前目录
chnSftp.cd(path);
}
}
this.cd("/");
return "1:创建目录成功";
}
/**
* 判断远程文件是否存在
* @param srcSftpFilePath
* @return
* @throws SftpException
*/
public boolean isFileExist (String srcSftpFilePath) throws SftpException {
boolean isExitFlag = false;
// 文件大于等于0则存在文件
if (getFileSize(srcSftpFilePath) >= 0) {
isExitFlag = true;
}
return isExitFlag;
}
/** 得到远程文件大小
* @see 返回文件大小
* @param srcSftpFilePath
* @return 返回文件大小,如返回-2 文件不存在,-1文件读取异常
* @throws SftpException
*/
public static long getFileSize (String srcSftpFilePath) throws SftpException {
long filesize = 0;//文件大于等于0则存在
try {
SftpATTRS sftpATTRS = chnSftp.lstat(srcSftpFilePath);
filesize = sftpATTRS.getSize();
} catch (Exception e) {
filesize = -1;//获取文件大小异常
if (e.getMessage().toLowerCase().equals("no such file")) {
filesize = -2;//文件不存在
}
}
return filesize;
}
/**
* 关闭资源
* @param fos 文件输出流
*/
public void close (FileOutputStream fos) {
try {
if (fos != null) {
fos.close();
}
if (channel.isConnected()) {
channel.disconnect();
log.info("Channel connect disconnect!");
}
if (session.isConnected()) {
session.disconnect();
log.info("Session connect disconnect!");
}
log.info("关闭资源连接");
} catch (IOException e) {
log.error("关闭资源时出现异常", e);
}
}
/**
* inputStream类型转换为byte类型
* @param iStrm
* @return
* @throws IOException
*/
public byte[] inputStreamToByte (InputStream iStrm) throws IOException {
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
int ch;
while ((ch = iStrm.read()) != -1) {
bytestream.write(ch);
}
byte imgdata[] = bytestream.toByteArray();
bytestream.close();
return imgdata;
}
/**
* 下载文件
* @param ftpFileName 具体的远程路径+文件名称
* @param localDir 存放本地文件的路径
* @throws SftpException
* @throws JSchException
*/
public void down(String remotePath ,String ftpFileName, String localDir) throws JSchException, SftpException {
open();
downFileOrDir(remotePath,ftpFileName, localDir);
close(fos);
}
/**
* 下载文件
*
* @param remotepath 远程文件的目录路径 "/dcds/abc/"
* @param ftpFileName 远程文件名称 "abc.txt"
* @param localDir 将远程文件下载后 ,存放文件的路径(本地)"d://temp"
*/
private void downFileOrDir(String remotepath ,String ftpFileName, String localDir) {
try {
//1 得到当前工作目录地址
log.info("操作1 得到当前工作目录地址:"+pwd());
//2 改变目录为配置的远程目录
cd(remotepath);
log.info("操作2 改变目录为配置的远程目录:"+pwd());
File localfile = new File(localDir + File.separator +ftpFileName);
localfile.createNewFile();
byte[] b = getByteArray(ftpFileName);
fos = new FileOutputStream(localfile);
fos.write(b);
log.info("下载文件到"+localDir+"成功!");
} catch (Exception e) {
log.error("下载失败!", e);
}
}
public static void main(String[] args) {
try {
SftpUtils sftp = new SftpUtils("mysftp", "123456", "192.168.4.135", 22);
sftp.down("/dcds/","0162430D.w02.gz", "f://temp");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
总结 : 这个功能不算复杂,不过如果不认真可能要花费好长时间,技术的成长不能一簇而就,需要我们长时间的积累。