一、SFTP搭建
在一些情况下(如需要为商户提供对账文件),你需要一台文件服务器存放这些文件,并允许用户登陆这台文件服务器传输(上传和下载)文件,但是不允许使用SSH方式(如secureCRT)登录文件服务器,你只允许其访问指定根目录(/home/sftp)下文件,这样SFTP服务就能很好满足这种需求。sftp传输数据(文件)使用的依旧是SSH协议,linux开启了sshd就相当于开启了SFTP。注:搭建SFTP文件服务器不需求安装什么额外的包,除了SSH,ssh需要OpenSSH4.8p1以后的版本。因为要使用chroot设置根目录(用户能看见和访问的根目录)。
查看SSH版本
ssh -V
搭建步骤:
使用root操作
1、建SFTP组
groupadd sftp
创建完成后使用 cat /etc/group可以看到
2、创建用户 albert
useradd -g sftp -s /sbin/nologin -M albert
-g 加入到sftp组
-s 禁止ssh登陆
-M 不要自动建立用户的登陆目录(在/home下)
3、为albert用户设置密码
passwd albert
alan123456
重复输入两次同一密码
4、创建sftp存放文件的目录并设置文件拥有者
mkdir -p /dala/sftp/verifyfile
chown -R albert:sftp /data/sftp/verifyfile
chmod 755 /data/sftp/verifyfile
5、编辑配置文件/etc/ssh/sshd_config
注:修改配置文件前先备份原有正常配置文件,方面还原
cp /etc/ssh/sshd_confiig /etc/ssh/sshd_config.bak
vi /etc/ssh/sshd_config
注释掉Subsystem sftp /usr/libexec/openssh/sftp-server
在其下面添加
Subsystem sftp internal-sftp
在文件最后添加
Match Group sftp # 指定活动目录
ChrootDirectory /data/sftp # 指定根目录
ForceCommand internal-sftp # 强制执行内部SFTP,并忽略任何~/.ssh/rc中的命令
AllowTcpForwarding no # 禁用端口转发
X11Forwarding no # 禁用X11图形界面转发
6、重启sshd
service sshd restart
7、测试
sftp albert@ip
输入密码即可连接上
8、权限控制
按照上面的设置
chown albert:sftp /data/sftp/verifyfile
chmod 755 /data/sftp/verifyfile
albert是拥有/data/sftp/verifyfile文件的读写权限的
sftp组其他用户的权限是5,即没有写入权限
如新增用户 jack
useradd -g sftp -s /sbin/nologin -M jack
passwd jack
jack123456
重复输入两次同一密码
那么jack的权限只有5
9、以上只是基本操作,但可基于此依据你自己需求(用户、文件目录、权限)自行设计专属sftp文件服务器,对于不断新增特定目录、特定权限用户的操作,可以配合sheel脚本使用或基于脚本再开发前端页面方面操作。
10、rsa认证方式设置和登录
以上介绍的SFTP用户登录方式是sshd_config配置文件默认打开的密码验证方式。
PasswordAuthentication yes
再介绍一种实际中使用更多,更安全,更方面的用户登录验证方式,即密钥登录。
密钥登录无需用户设置密码,通过rsa密钥对加解密验证,在客户端和服务器端建立安全的连接,简单地说,public key放在服务器端,即下面配置的authorized_keys,private key放在客户端,客户端发起请求连接,服务器根据请求用户名识别对应客户端公钥,sshd服务产生一个随机数,用public key进行加密后,发回到客户端,客户端用private key解密得到该随机数,客户端将解密后的随机数发回服务器端,服务端进行匹配,匹配成功认证通过,允许登录。这种方式避免了密码暴力破解尝试的危险,当然密钥认证因为有加解密和随机数传输验证的过程,连接耗时自然比密码方式长些。
我们再新增可登录用户qingniu , 采用密钥验证方式登录
依旧用root操作
useradd -g sftp -s /sbin/nologin qingniu
不要再使用passwd为其设置密码
注意这里不要 -M , 因为我们需要useradd为新增用户在/home目录下自动建立用户的登陆目录,我们需要再/home/qingniu目录下设置密钥文件。
cd /home/qingniu
ll -a -- 可看到所有隐藏目录
mkdir .ssh --存放密钥文件和认证文件
ssh-keygen -t rsa
输入/home/qingniu/.ssh/id_rsa_qingniu
按回车键,不对密钥加密
再次按回车确认
成功在/home/qingniu/.ssh目录下生成一对密钥id_rsa_qingniu 、id_rsa_qingniu.pub
cd /home /qingniu/.ssh -- 进入密钥对所在目录
cat id_rsa_qingniu.pub >> authorized_keys -- 往authorized_keys文件中写入公钥key
接下来就是新增目录、密钥的权限和用户组设置,很重要,因为我们是用root用户操作这些目录和文件的。至于为什么按下面设置,请自行思考,其实也很简单。
chmod 600 /home/qingniu/.ssh/authorized_keys
chmod 700 /home/qingniu/.ssh
chown -R qingniu:sftp /home/qingniu/.ssh
为/etc/ssh/sshd_config添加配置
PermitRootLogin yes
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ss:h/authorized_keys
添加完成后保存再重启sshd服务
service sshd restart
把私钥id_rsa_qingniu导出,重新打开FileZilla用密钥认证方式登录测试
如果你只允许用户使用密钥认证登录,可以设置/etc/ssh/sshd_config文件
PasswordAuthentication no
如果不改,密钥认证和密钥认证两种登录方式都允许。
二、客户端连接
FileZilla下载地址:https://filezilla-project.org/download.php
按照安装引导安装即可
使用上面SFTP用户登陆测试
密码认证方式
输入 主机IP username password port(默认22)
密钥认证方式
编辑 -> 设置 -> sftp -> 添加密钥文件 -> 确认
输入 主机IP username 不填密码 port(默认22)
三、java代码连接
使用java代码语言实现与sftp服务器的文件传输,我们可以使用Jcraft公司开发的JSch包,JSch使用纯java语言实现了ssh2协议,感谢ssh2标准协议的制定,才使得我们能通过各种形态的客户端与服务器之间建立安全可靠的通信。因此实现了SSH2协议的JSch能做的也就不仅仅是文件传输,包括ssh协议实现的X11 GUI转发、端口转发、终端仿真等各种酷炫功能,所以学习JSch项目对于java程序员是有很必要的,那就从文件传输这个功能开始接触JSch这个项目吧。
首先在项目中引入JSch包
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
基于JSch包实现的SFTP服务器文件上传和下载功能代码如下:
package com.qing.niu.communication.sftp;
import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* <p>
* Sftp工具类
* </p>
*
* @author huqingniu
* @version 1.0.0
* @date 2018/12/24
*/
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"));
}
}