天天看点

JSCH通过密码和公钥连接SFTP服务器的指纹判断以及Serv-U设置公钥登录(零)背景(一)主机指纹(二)JSCH程序公钥登录(三)Serv-U允许公钥方式(四)Java登录代码(五)后记

文章目录

  • (零)背景
  • (一)主机指纹
    • 1.1 现象和原理
    • 1.2 解决方法
      • 1.2.1手动ssh
      • 1.2.2 执行ssh-keyscan
      • 1.2.3 程序用公钥登录
    • 1.3 指纹变化
  • (二)JSCH程序公钥登录
    • 2.1 注意密钥格式
  • (三)Serv-U允许公钥方式
    • 3.1 设置用户公钥文件
    • 3.2 配置公钥登录
  • (四)Java登录代码
  • (五)后记

(零)背景

随着安全要求越来越严格,逐渐内网的FTP服务器都被替换为了基于SSH的SFTP服务器。

旧的程序也都被替换成了Java程序用JSCH来连接FTP/SFTP并下载文件。

虽然SFTP是基于SSH,但一般都是设置成用户名+密码的方式访问的。

PS:之前遇到相关的问题:

键盘交互:《SFTP服务器认证类型导致无法登录的问题》

认证指令:《华为网元设备客户端连接我方FTP服务端Serv-U的问题》

(一)主机指纹

1.1 现象和原理

第一个小问题是主机身份(指纹)判断,如果你百度了一些说法,不严格验证主机指纹,那么连接倒是OK了。但后续无法保证不受到中间人攻击(MITM Attack),简单说就是有人通过伪装成目标主机,获得你的身份信息。

所以严格验证主机指纹还是需要的:

这时直接登录会报异常:

reject HostKey

1.2 解决方法

当然就是记录已知主机到.ssh目录known_hosts文件中。

格式是每一行一个主机,最好把同一台主机弄成一行,不要每个IP都一行很冗余,如下:

host1,ip1,ip2,ip3,ip4 ssh-rsa AAABBBCCCxxyyzzxxyyzz==
host2,ipv4,ipv6 ssh-rsa DDDEEEFFFxxyyzzxxyyzz==
           

Windows下正常用户位置:

C:\Users\用户名\.ssh\known_hosts

Windows下系统服务认的“用户目录”位置:

%SystemRoot%\System32\config\systemprofile\.ssh\known_hosts

Linux下位置:

/home/用户名/.ssh/known_hosts

那么如何得到主机的身份(指纹)信息呢?

有下面三个办法:

1.2.1手动ssh

先手动ssh连接一次这台确认过的服务器。

$shion@shionlnx ~> ssh 主机名或IP
           

当你同意连接时,主机的信息会被记录到用户目录的known_hosts中。

一个主机因为多个IP或名称可能会重复多条,可以自行整理成上面的单条格式。

1.2.2 执行ssh-keyscan

或者,执行这个指令可以得到目标主机的身份(指纹)信息。

1.2.3 程序用公钥登录

如果程序用公钥方式登录,那么和手动SSH类似。

第一次执行时,主机的信息会被记录到用户目录的known_hosts中。

今后程序就不会再提示未知指纹了(见最后的代码)。

PS:密码方式登录不会自动记录。

1.3 指纹变化

如果发生中间人攻击,或者主机指纹确实发生了变化,无论程序还是手动SSH登录都会得到大概下面的错误,并且不会让你继续登录。

程序:

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that the RSA host key has just been changed.
The fingerprint for the RSA key sent by the remote host 192.168.168.XX is
aa:bb:cc:11:22:33:44:55:66:77:88:99:00:dd:ee:ff.
Please contact your system administrator.
Add correct host key in /home/用户名/.ssh/known_hosts to get rid of this message.
           

手动:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:saj23d4f6k7a8sjfkdlsdfjk/3dS8juih3ys.
Please contact your system administrator.
Add correct host key in /home/用户名/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/用户名/.ssh/known_hosts:1
  remove with:
  ssh-keygen -f "/home/用户名/.ssh/known_hosts" -R "shion-haier"
RSA host key for shion-haier has changed and you have requested strict checking.
Host key verification failed.
           

如果确认主机没有问题,只是别的原因指纹变化了。

那么需要从known_hosts中删除对应的那条记录,重新记录一次新的主机身份(指纹)。

(二)JSCH程序公钥登录

如果还没设好密钥登录,那么请自行先设置好,用ssh指令试试看。

可以参考我的:《从零开始学习大数据平台(Episode 1)》里面

章节(1.3)设置虚拟机环境【4】配置ssh密钥方式登录(免密码登录)

如果你已经设置好了密钥并且可以手动ssh免密码登录,程序用JSCH就可以这样:

jsch.addIdentity(thePrivateKeyFileName); //直接用私钥文件名
...
session = jsch.getSession(username, host, port);
session.setConfig("PreferredAuthentications", "publickey"); //指定验证方式。
           

函数addIdentity大概这样的:

//com.jcraft.jsch.JSch 
//Maven: com.jcraft:jsch:0.1.55 (jsch-0.1.55.jar)

public void addIdentity(String prvkey) throws JSchException
//Sets the private key, which will be referred in the public key authentication.

Parameters:
//prvkey - filename of the private key.

Throws:
//JSchException - if prvkey is invalid.
           

2.1 注意密钥格式

设置ssh密钥登录(免密码登录)时。

生成的密钥由于工具或版本的原因,JSCH可能用不了。

需要这样:

$shion@shionlnx ~> ssh-keygen -m PEM -t rsa
           

指定rsa加密方式(目前还是默认),并用了参数 -m PEM指定格式:

-m key_format
 Specify a key format for key generation, the -i (import), -e (export) conversion options, 
 and the -p change passphrase operation.  
 The latter may be used to convert between OpenSSH private key and PEM private key formats. 
 The supported key formats are: “RFC4716” (RFC 4716/SSH2 public or private key), “PKCS8” (PKCS8 public or private key) or “PEM” (PEM public key).  
 By default OpenSSH will write newly-generated private keys in its own format, 
 but when converting public keys for export the default format is “RFC4716”.  
 Setting a format of “PEM” when generating or updating a supported private key type will cause the key to be stored in the legacy PEM private key format.
           

(三)Serv-U允许公钥方式

这个弄了很久一直搞不懂。

最后发现只是一个选项的问题……

3.1 设置用户公钥文件

新建或选中一个用户 >> User Information >> SSH Keys >> ManageKeys

JSCH通过密码和公钥连接SFTP服务器的指纹判断以及Serv-U设置公钥登录(零)背景(一)主机指纹(二)JSCH程序公钥登录(三)Serv-U允许公钥方式(四)Java登录代码(五)后记

Add Key,增加一个公钥文件。

这个公钥不需要是当前用户名生成的(呃……这也……)。

当然也可以用它Create Key来新建密钥。

但是这样违背了SSH的安全原则,也就是说Serv-U是服务方,它应该只保存用户的公钥(从客户机拷贝过来)而不是生成私钥拷贝到客户机(私钥不应在计算机间传输)。

JSCH通过密码和公钥连接SFTP服务器的指纹判断以及Serv-U设置公钥登录(零)背景(一)主机指纹(二)JSCH程序公钥登录(三)Serv-U允许公钥方式(四)Java登录代码(五)后记

3.2 配置公钥登录

1)在你需要登录的用户的【Limits & Settings】下。或者全局的【Limits & Settings】的 Limits页。

2)选中【Limit Type】下拉框,切换到【Password】。

3)定位到【SSH authentication type】项目双击它。PS:由于默认项目的不能编辑,所以双击不是修改而是新建一条同命项目【SSH authentication type】。

4)将 Password and Public Key 改为 Password or Public Key 。⭐️

虽然让人不太理解……

我改的是全局,大概截图如下:

JSCH通过密码和公钥连接SFTP服务器的指纹判断以及Serv-U设置公钥登录(零)背景(一)主机指纹(二)JSCH程序公钥登录(三)Serv-U允许公钥方式(四)Java登录代码(五)后记

如果不改这一项,那么你将得到返回是:

Auth fail

因为觉得不理解不爽,所以又看了一下Serv-U的日志。

发现没有任何帮助……

设置 Password and Public Key的失败记录:

[02] Mon 13Dec21 17:38:43 - (000003) Connected to 192.168.50.88 (local address 192.168.50.88, port 22)
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: none
[31] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_FAILURE: login failed
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[31] Mon 13Dec21 17:38:43 - (000003) SSH_MSG_DISCONNECT: client has requested a disconnect.  Reason code: 3
[02] Mon 13Dec21 17:38:43 - (000003) Closed session
           

设置 Password or Public Key 的成功记录:

[02] Mon 13Dec21 17:42:12 - (000004) Connected to 192.168.50.88 (local address 192.168.50.88, port 22)
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: none
[31] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_FAILURE: login failed
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[02] Mon 13Dec21 17:42:13 - (000004) User "Shion" logged in
[31] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_SUCCESS: successful login
           

(四)Java登录代码

考虑到实际场景,先尝试严格方式,如果成功则日志记录指纹已知。

但是如果没有设置known_hosts则再尝试不严格的方式,并告警指纹未知有风险。

如果密码是一个私钥文件名并且文件存在,则采用密钥方式登录,否则密码方式登录。

UserInfo 部分请继续参考键盘交互:《SFTP服务器认证类型导致无法登录的问题》

public void getConnect(String host, int port, String username,String password, boolean psv) throws Exception {
		fpassword=password;
		UserInfo ui = new MyUserInfo();
		try {
			JSch jsch = new JSch();
			String knownHosts = System.getProperty("user.home")+"/.ssh/known_hosts";
			if (new File(knownHosts).exists()) {
				jsch.setKnownHosts(knownHosts);
			}
			if (new File(password).exists()) { //如果密码位置输入的是密钥文件
				jsch.addIdentity(password);
			}
			session = jsch.getSession(username, host, port);
			session.setUserInfo(ui);
			session.setConfig("userauth.gssapi-with-mic", "no");
			session.setConfig("StrictHostKeyChecking", "yes"); // 验证 HostKey

			if (new File(password).exists()){ //如果密码位置输入的是密钥文件
				session.setConfig("PreferredAuthentications", "publickey"); // 公钥校验
			} else {
				session.setConfig("PreferredAuthentications", "password");
				session.setPassword(password);
			}
			session.connect(5000);
			CrossLog.MyLog(TNU.LogType.INFO,"主机指纹: ["+session.getHostKey().getFingerPrint(jsch)+"]已知");
		} catch (JSchException jse) {
			if (jse.getMessage().contains("reject HostKey")) {
				JSch jsch = new JSch();
				String knownHosts = System.getProperty("user.home")+"/.ssh/known_hosts";
				if (new File(knownHosts).exists()) {
					jsch.setKnownHosts(knownHosts);
				}
				if (new File(password).exists()) { //如果密码位置输入的是密钥文件
					jsch.addIdentity(password);
				}
				session = jsch.getSession(username, host, port);
				session.setUserInfo(ui);
				session.setConfig("userauth.gssapi-with-mic", "no");
				session.setConfig("StrictHostKeyChecking", "no"); // 不验证 HostKey

				if (new File(password).exists()){ //如果密码位置输入的是密钥文件
					session.setConfig("PreferredAuthentications", "publickey"); // 公钥校验
				} else {
					session.setConfig("PreferredAuthentications", "password");
					session.setPassword(password);
				}
				session.connect(5000);
				CrossLog.MyLog(TNU.LogType.WARNING,"主机指纹: ["+session.getHostKey().getFingerPrint(jsch)+"]未知!");
				CrossLog.MyLog(TNU.LogType.WARNING,"请手动确认主机指纹,避免中间人攻击(MITM Attack)危险!");
			}
			else {
				throw new Exception("连接服务器失败(JSCH)! "+jse.getMessage());
			}
		} catch (Exception e) {
			if (session.isConnected())
				session.disconnect();
			throw new Exception("连接服务器失败! "+e.getMessage());
		}
		channel = session.openChannel("sftp");
		try {
			channel.connect();
		} catch (Exception e) {
			if (channel.isConnected())
				channel.disconnect();
			throw new Exception("连接服务器失败(channel)! "+e.getMessage());
		}
		sftp = (ChannelSftp) channel;
	}
           

(五)后记

实际工作中,用到SFTP都是管理员分配给我们一个用户名和密码,所以无法密钥登录方式。

但软件功能完善一点,总归是好的🌝

the end