简述
SFTP可以为传输文件提供一种安全的网络的加密方法。sftp 与 ftp 有着几乎一样的语法和功能。SFTP 为 SSH的其中一部分,是一种传输档案至服务器的安全方式。其实在SSH软件包中,已经包含了一个叫作SFTP(Secure File Transfer Protocol)的安全文件信息传输子系统,SFTP本身没有单独的守护进程,它必须使用sshd守护进程(端口号默认是22)来完成相应的连接和答复操作,所以从某种意义上来说,SFTP并不像一个服务器程序,而更像是一个客户端程序。SFTP同样是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多,如果您对网络安全性要求更高时,可以使用SFTP代替FTP。linux开启了sshd就相当于开启了SFTP。
搭建步骤
描述:创建两个用户,一个为sftp管理员,剩余一个为指定目录的访问用户。sftp管理员对普通用户的sftp根目录下的内容具有读写权限,限制其他用户只能访问其自己的根目录且仅有读权限;相关的sftp用户不能登录到Linux系统中。
sftp的普通认证
1:确认openssh的版本:ssh -V(要求版本高于4.8p1,低于此版本需升级)
2:创建sftp管理组及用户组
[root@localhost ~]# groupadd sftpadmin
[root@localhost ~]# groupadd sftp
3:创建sftp管理用户及普通用户(-g 加入到sftp组 ,-s 禁止ssh登陆)
[root@localhost ~]# useradd -g sftp -s /bin/false yeamin #/bin/false也可以替换为/sbin/nologin,目的是不允许该用户登录到系统中
[root@localhost ~]# useradd -g sftpadmin -s /bin/false admin
[root@localhost ~]# passwd yeamin
[root@localhost ~]# passwd admin
4:分别创建对应用户的sftp根目录并指定为其家目录
[root@localhost ~]# mkdir -pv /data/sftp/yeamin/share
mkdir: created directory `/data'
mkdir: created directory `/data/sftp'
mkdir: created directory `/data/sftp/yeamin'
mkdir: created directory `/data/sftp/yeamin/share'
注:多个用户可以写成 mkdir -pv /data/sftp/{A,B,C}/share
5:配置sshd_config文件
[root@localhost ~]# vim /etc/ssh/sshd_config
#Subsystem sftp /usr/libexec/openssh/sftp-server(找到如下这行,用#符号注释掉,大致在文件末尾处。)
Subsystem sftp internal-sftp #这行指定使用sftp服务使用系统自带的internal-sftp
Match Group sftp #这行用来匹配sftpusers组的用户,如果要匹配多个组,多个组之间用逗号分割;
ChrootDirectory /data/sftp/%u #用chroot将用户的根目录指定到%h,%h代表用户home目录,这样用户就只能在用户目录下活动。也可用%u,%u代表用户名。
ForceCommand internal-sftp #指定sftp命令
AllowTcpForwarding no
X11Forwarding no
Match user sftpadmin #匹配用户了,多个用户名之间也是用逗号分割
ChrootDirectory /data/sftp
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
6:- 设置Chroot目录的权限
[root@localhost ~]# chown root:sftp /data/sftp/yeamin #修改普通用户的根目录属组
[root@localhost ~]# chmod 755 /data/sftp/yeamin #修改普通用户的根目录权限
[root@localhost ~]# chown root:sftpadmin /data/sftp/ #修改管理员的根目录属组
[root@localhost ~]# chmod 755 /data/sftp/ #修改管理员根目录的权限
[root@localhost ~]# chown sftpadmin:sftp /data/sftp/yeamin/share/ #修改各普通用户下的share目录的属主为管理员,属组为普通用户组
[root@localhost ~]# chmod 750 /data/sftp/yeamin/share/ #各share目录管理员的权限为读写,sftp组仅有读权限,其他用户没有权限访问
7:关闭selinux
[root@localhost ~]# vim /etc/selinux/config
SELINUX=permissive
[root@localhost ~]# setenforce 0
8:重启sshd服务:service sshd restart
9:验证sftp登录:sftp username@IP
10:设置记录sftp服务器的登录及操作日志
既然搭建了sftp服务器,那么通常还是需要知道哪些人在什么时候做了哪些操作,默认情况下sftp服务只会记录相关的登录信息,默认保存在/var/log/audit/audit.log日志下。但是默认的记录方式并不便于管理员获取指定的信息,因此我们可以对sftp日志进行另行的设置指定。
10.1:修改/etc/ssh/sshd_config
[root@localhost ~]# vim /etc/ssh/sshd_config
Subsystem sftp internal-sftp -l VERBOSE -f AUTH,local5 #设置日志级别为VERBOSE,并指定日志记录的收集设施
Match Group sftp
ChrootDirectory /data/sftp/%u
ForceCommand internal-sftp -l VERBOSE #设置日志级别为VERBOSE
AllowTcpForwarding no
X11Forwarding no
Match user sftpadmin
ChrootDirectory /data/sftp
ForceCommand internal-sftp -l VERBOSE #设置日志级别为VERBOSE
AllowTcpForwarding no
X11Forwarding no
10.2:修改rsyslog.conf
[root@localhost ~]# vim /etc/rsyslog.conf
auth,authpriv.*,local5.* /var/log/sftp.log #设置将相关的auth、authpriv及local5相关的日志信息记录到/var/log/sftp.log文件
11: 重启sshd和rsyslog
[root@localhost ~]# service sshd restart
[root@localhost ~]# service rsyslog restart
12:查看日志:cat /var/log/sftp.log
13:限制ssh连接的访问Ip
[root@localhost ~]# vim /etc/ssh/sshd_config
# Authentication:
AllowUsers root@10.10.10.* #限制root用户只能通过10.10.10.*网段登录访问
AllowUsers yeamin@10.10.10.* #限制yeamin用户只能通过10.10.10.*网段登录访问
sftp的密钥认证
密钥登录无需用户设置密码,通过rsa密钥对加解密验证,在客户端和服务器端建立安全的连接,简单地说,public key放在服务器端,即下面配置的authorized_keys,private key放在客户端,客户端发起请求连接,服务器根据请求用户名识别对应客户端公钥,sshd服务产生一个随机数,用public key进行加密后,发回到客户端,客户端用private key解密得到该随机数,客户端将解密后的随机数发回服务器端,服务端进行匹配,匹配成功认证通过,允许登录。这种方式避免了密码暴力破解尝试的危险,当然密钥认证因为有加解密和随机数传输验证的过程,连接耗时自然比密码方式长些。
1:生成秘钥:ssh-keygen -t rsa(回车之后会让输入秘钥加密的账户,密码以及秘钥保存的地址等,也可以不输入)
2:将公钥传输到目标服务器(如果authorized_keys文件已存在,则追加到该文件的后面)
[root@localhost ~]# cp /root/.ssh/id_rsa.pub /home/charles/.ssh/authorized_keys
3:设置秘钥相关的权限
chmod 600 /home/yeamin/.ssh/authorized_keys
chmod 700 /home/yeamin/.ssh
chown -R yeamin:sftp /home/yeamin/.ssh
ChrootDirectory设置的目录权限及其所有的上级文件夹权限,属主和属组必须是root; ChrootDirectory设置的目录权限及其所有的上级文件夹权限,只有属主能拥有写权限,也就是说权限最大设置只能是755。 4:为/etc/ssh/sshd_config添加配置
PermitRootLogin yes
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile /root/.ssh/authorized_keys #指出公钥的位置
注:如果你只允许用户使用密钥认证登录,可以设置/etc/ssh/sshd_config文PasswordAuthentication no
如果不改,密钥认证和密钥认证两种登录方式都允许。
5:测试密钥登录sftp
sftp -oIdentityFile=/root/.ssh/id_rsa username@IP
SSH/SSHD服务
- 查看状态:
systemctl status sshd.service - 启动服务:
systemctl start sshd.service - 重启服务:
systemctl restart sshd.service - 开机自启:
systemctl enable sshd.service
java代码实现
- pom.xml
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
- 基于JSch包实现的SFTP服务器文件上传和下载功能代码如下:
public class SftpTool {
public static final Logger logger = LoggerFactory.getLogger(SftpTool.class);
private String host;
private String username;
private int port;
public SftpTool(String host, String username, int port){
this.host = host;
this.username = username;
this.port = port;
}
/**
* 登陆sftp
*
* @param authTypeMode 认证方式
* @return 登陆信息
*/
public Map<String,Object> loginIn(AuthTypeMode authTypeMode){
//解决JSch日志打印问题
JSch.setLogger(new SettleJschLogPrint());
try {
ChannelSftp sftp = null;
Session session = null;
JSch jsch = new JSch();
logger.info("获取SFTP服务器连接username:{},host:{},port:{}",username,host,port);
session = jsch.getSession(username,host,port);
logger.info("连接成功建立");
if (AuthTypeEnum.RSA.getCode().equals(authTypeMode.getAuthType())){
jsch.addIdentity(authTypeMode.getAuthValue(),"");
}else {
session.setPassword(authTypeMode.getAuthValue());
}
Properties sshConfig = new Properties();
sshConfig.put("StrictHostKeyChecking","no");
sshConfig.put("PreferredAuthentications","publickey,gssapi-with-mic,keyboard-interactive,password");
session.setConfig(sshConfig);
session.connect();
logger.info("用户" + username + "成功登陆");
Channel channel = session.openChannel("sftp");
channel.connect();
sftp = (ChannelSftp) channel;
HashMap<String,Object> loginInfo = new HashMap<>();
loginInfo.put("sftp",sftp);
loginInfo.put("session",session);
return loginInfo;
} catch (JSchException e) {
throw new RuntimeException("user login SFTP server occur exception:" + e);
}
}
/**
* 退出登陆
*
* @param sftp sftp对象
* @param session session对象
*/
public void loginOut(ChannelSftp sftp, Session session){
try {
if(null != sftp && sftp.isConnected()){
sftp.disconnect();
}
if (null != session && session.isConnected()){
session.disconnect();
}
} catch (Exception e) {
logger.warn("用户退出SFTP服务器出现异常:" + e);
}
}
/**
* 下载文件
*
* @param downloadFilePath 要下载的文件所在绝对路径
* @param downloadFileName 要下载的文件名(sftp服务器上的文件名)
* @param saveFile 文件存放位置(文件所在绝对路径)
* @param authTypeMode 用户认证方式
*/
public void download(String downloadFilePath, String downloadFileName, File saveFile, AuthTypeMode authTypeMode) throws Exception{
Assert.notNull(downloadFilePath,"download file absolute path is not null");
Assert.notNull(downloadFileName,"download file is not null");
Assert.notNull(saveFile,"save file location is not null");
Assert.notNull(authTypeMode,"auth type way is not null");
OutputStream outputStream = null;
ChannelSftp sftp = null;
Session session = null;
try {
Map<String,Object> loginInfo = loginIn(authTypeMode);
sftp = (ChannelSftp) loginInfo.get("sftp");
session = (Session) loginInfo.get("session");
logger.info("待下载文件地址为:" + downloadFilePath + ",文件名为:" + downloadFileName + ",认证方式:" + authTypeMode.getAuthValue());
sftp.cd(downloadFilePath);
sftp.ls(downloadFileName);
outputStream = new FileOutputStream(saveFile);
sftp.get(downloadFileName,outputStream);
logger.info("文件下载完成!");
} finally {
if (null != outputStream){
outputStream.close();
}
loginOut(sftp,session);
}
}
/**
* 上传文件
*
* @param uploadPath 上传SFTP完整路径
* @param uploadFile 上传文件(完整路径)
* @param authTypeMode 认证方式
*/
public void upload(String uploadPath, String uploadFile, AuthTypeMode authTypeMode) throws Exception{
Assert.notNull(uploadPath,"upload path is not null");
Assert.notNull(uploadFile,"upload file is not null");
Assert.notNull(authTypeMode,"auth type way is not null");
InputStream inputStream = null;
ChannelSftp sftp = null;
Session session = null;
try {
Map<String,Object> loginInfo = loginIn(authTypeMode);
sftp = (ChannelSftp) loginInfo.get("sftp");
session = (Session) loginInfo.get("session");
logger.info("待上传文件为:" + uploadFile + ",上传SFTP服务器路径:" + uploadPath + ",认证方式:" + authTypeMode.getAuthValue());
File file = new File(uploadFile);
inputStream = new FileInputStream(file);
try {
sftp.cd(uploadPath);
} catch (SftpException e) {
logger.error("SFTP器服务存放文件路径不存在");
throw new RuntimeException("upload path is not exist");
}
sftp.put(inputStream,file.getName());
logger.info("上传文件成功!");
} finally {
if (null != inputStream){
inputStream.close();
}
loginOut(sftp,session);
}
}
/**
* authType = PASSWORD,authValue = password
* authType = rsa,authValue = rsa文件路径
*/
class AuthTypeMode{
private String authType;
private String authValue;
AuthTypeMode(String authType, String authValue){
this.authType = authType;
this.authValue = authValue;
}
String getAuthType(){
return authType;
}
String getAuthValue(){
return authValue;
}
}
/**
* 在slf4j日志框架里打印JSch日志
*/
class SettleJschLogPrint implements com.jcraft.jsch.Logger{
@Override
public boolean isEnabled(int i) {
return true;
}
@Override
public void log(int i, String s) {
logger.info(s);
}
}
enum AuthTypeEnum {
PASSWORD("PASSWORD","密码认证"),
RSA("RSA","rsa密钥认证");
private String code;
private String desc;
AuthTypeEnum(String code, String desc){
this.code = code;
this.desc = desc;
}
public String getCode(){
return this.code;
}
}
public static void main(String[] args) throws Exception{
//密码认证方式
SftpTool sftpTool = new SftpTool("192.168.79.151","albert",22);
//下载文件
File saveFile = new File("/data/sftp/verifyfile_01.txt");
sftpTool.download("/verifyfile","verifyfile_01.csv",saveFile,sftpTool.new AuthTypeMode(AuthTypeEnum.PASSWORD.getCode(),"alan123456"));
//上传文件
sftpTool.upload("/verifyfile","/data/sftp/verifyfile_01.txt",sftpTool.new AuthTypeMode(AuthTypeEnum.PASSWORD.getCode(),"alan123456"));
//密钥认证方式下载文件
SftpTool sftpToolTwo = new SftpTool("192.168.79.151","qingniu",22);
File saveFileTwo = new File("/data/sftp/verifyfile_01.txt");
sftpToolTwo.download("/verifyfile","verifyfile_01.csv",saveFileTwo,sftpToolTwo.new AuthTypeMode(AuthTypeEnum.RSA.getCode(),"/data/rsa/id_rsa_qingniu"));
//密钥认证方式上传文件( 上传会失败Permission denied,因为qingniu这个用户没有写的权限 )
sftpToolTwo.upload("/verifyfile","/data/sftp/verifyfile_01.txt",sftpToolTwo.new AuthTypeMode(AuthTypeEnum.RSA.getCode(),"/data/rsa/id_rsa_qingniu"));
}
}