JSch - 配置SFTP服务器SSH免密登录

发布时间 2023-04-20 15:22:07作者: lihewei


需求:做一个通过ssh免密登录的需求,是基于原先密码登录sftp服务器的代码上进行改造

1. 什么是SFTP

SFTP是一个安全文件传送协议,可以为传输文件提供一种安全的加密方法。SFTP 为 SSH的一部份,是一种传输文件到服务器的安全方式。SFTP是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多,如果您对网络安全性要求更高时,可以使用SFTP代替FTP。

2. 什么是Jsch以及它的作用

Jsch是一个纯粹的用java实现SSH功能的java library。如果要知道Jsch的功能需先了解一下SSH。SSH是一个安全协议,用来在不同系统或者服务器之间进行安全连接,在连接和传送数据的过程中会进行加密。SSH一般是基于客户端的或者Linux命令行,比如window同过OpenSSH、putty等客户端的工具,在linux上可以通过ssh username@host命令进行连接。但是如果在Java中如何实现SSH呢?这时候便是通过JSCH来实现此的功能。

3. sftp服务器认证机制

Jsch提供了四种认证机制:

  • password 密码方式
  • publickey(DSA,RSA) 公私钥方式
  • keyboard-interactive
  • gss-api-with-mic

其中publickey方式通过配置公私钥实现SSH免密登录,这里也只是简单讲一下它的使用。

4. publickey和password两种方式登录sftp的API调用

SSH公钥检查机制:

公钥检查机制是一个安全机制,可以防范中间人劫持等黑客攻击。SSH连接远程主机时,会检查主机的公钥。如果是第一次该主机,会显示该主机的公钥摘要,提示用户是否信任该主机。当选择接受,就会将该主机的公钥追加到文件 ~/.ssh/known_hosts 中。当再次连接该主机时,就不会再提示该问题了。 但是在某些特殊的情况下,严格的SSH公钥检查可能会破坏一些依赖SSH协议的自动化任务如Java的Jsch免密登录sftp程序。解决方式为调整StrictHostKeyChecking配置指令。StrictHostKeyChecking选项如下3种:

session.setConfig("StrictHostKeyChecking", "no/ask/yes?");
  • no 最不安全的级别,当然也没有那么多烦人的提示了,相对安全的内网测试时建议使用。如果连接server的key在本地不存在,那么就自动添加到文件中(默认是known_hosts),并且给出一个警告。
  • ask 默认的级别,就是出现刚才的提示了。如果连接和key不匹配,给出提示,并拒绝登录。
  • yes 最安全的级别,如果连接与key不匹配,就拒绝连接,不会提示详细信息。

下面根据password来分析publickey方式与其区别:

  • 原来password方式需要这样一段代码来设置密码:session.setPassword (properties.getPassword ()); ,但是ssh key的方式就没有password了,所以这段要删掉。
  • publickey需要设置我们的ssh私钥文件的全路径(privateKeyFile):jsch.addIdentity (properties.getPrivateKeyFile ());
  • 一般私钥文件需要口令(passphrase)才能读取,这需要设置一个配置类对象,在jsch里其实需要自己搞一个简单的接口实现(如下:SftpAuthKeyUserInfo类 ),然后增加:session.setUserInfo(new SftpAuthKeyUserInfo (properties.getPassphrase ()));

因此代码可以如下改造:

  1. 设置配置类对象

    SftpAuthKeyUserInfo.java

import com.jcraft.jsch.UserInfo;
import lombok.extern.slf4j.Slf4j;

/**
 * ssh private key passphrase info
 */
@Slf4j
public class SftpAuthKeyUserInfo implements UserInfo {
    /**
     * ssh private key passphrase
     */
    private String passphrase;

    public SftpAuthKeyUserInfo (String passphrase) {
        this.passphrase = passphrase;
    }

    @Override
    public String getPassphrase() {
        return passphrase;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public boolean promptPassphrase(String s) {
        return true;
    }

    @Override
    public boolean promptPassword(String s) {
        return false;
    }

    @Override
    public boolean promptYesNo(String s) {
        return true;
    }
    
    @Override
    public void showMessage(String message) {
        log.info ("SSH Message:{}", message);
    }
}
  1. 改造以适配publickey登录方式
    JSch jsch = new JSch();
    if (StringUtils.isNotBlank(properties.getPrivateKeyFile())) {
        jsch.addIdentity(properties.getPrivateKeyFile());
    }
    // 根据用户名,主机ip,端口获取一个Session对象
    Session session = jsch.getSession(properties.getUsername(), properties.getHost(), properties.getPort());
	//改造以适配publickey登录方式
    switch (properties.getAuthType()) {
        case PASSWORD:
            Objects.requireNonNull(properties.getPassword());
            session.setPassword(properties.getPassword());
            break;
        case PUBLIC_KEY:
            Objects.requireNonNull(properties.getPrivateKeyFile());
            if (StringUtils.isBlank(properties.getPassphrase())) {
                throw new IllegalArgumentException("口令不为能空(私钥未加密时填任意值)");
            }
            session.setUserInfo(new SftpAuthKeyUserInfo(properties.getPassphrase()));
            break;
    }
    // 设置timeout时间
    session.setTimeout(properties.getConnectTimeout());
    // 设置keep-alive消息发送间隔(milliseconds)
    session.setServerAliveCountMax(properties.getServerAliveCountMax());
    // 设置发送keep-alive消息的最大次数
    session.setServerAliveInterval(properties.getServerAliveInterval());
    //第一次登陆时候,是否需要提示信息
    session.setConfig("StrictHostKeyChecking", "no");
    //设置ssh的DH秘钥交换
    session.setConfig("kex", "diffie-hellman-group1-sha1");
    //跳过Kerberos username 身份验证提示
    session.setConfig("PreferredAuthentications", "publickey,keyboard-interactive,password");
    // 通过Session建立链接
    synchronized (properties) {
        session.connect();
    }
    // 打开SFTP通道
    ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
    // 建立SFTP通道的连接
    channel.connect();
    if (log.isDebugEnabled()) {
        log.debug("SSH Channel connected.session={},channel={}", session, channel);
    }

properties为自定义sftp服务端配置:

public class SftpServerProperties {

    /**
     * sftp服务器安全验证方式,默认口令认证登录
     */
    private EnumSftpAuthType authType = EnumSftpAuthType.PASSWORD;

    /**
     * sftp服务器IP地址
     */
    private String host;

    /**
     * 端口号
     */
    private int port = 22;

    /**
     * sftp用户名
     */
    private String username;

    /**
     * sftp用户密码
     */
    private String password;

    /**
     * sftp服务器根路径
     */
    private String root = "/";

    /**
     * ssh private key文件路径
     */
    private String privateKeyFile;

    /**
     * ssh private key passphrase
     */
    private String passphrase;

    /**
     * socket连接和读取超时时间 5 min
     */
    public int connectTimeout = 300000;

    /**
     * 发送keep-alive消息的间隔(milliseconds)
     * PS:在隧道无通信后,定时发送一个请求给服务器要求服务器响应,保证终端不会因为超时空闲而断开连接
     */
    private int serverAliveInterval = 0;

    /**
     * 向服务器发送保持活动消息的最大次数
     * PS:向服务器发送keep-alive消息,但接收不到任何响应的情况下,达成最大次数限制后,SSH客户端就自动断开连接并退出,将控制权交给你的监控程序
     */
    private int serverAliveCountMax = 1;
}